using EgwProxy.Ftp;
using IOB_UT_NEXT;
using MapoSDK;
using Newtonsoft.Json;
using NLog;
using NLog.Targets;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.AccessControl;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Serialization;
using YamlDotNet.Core.Tokens;
using static IOB_UT_NEXT.BaseAlarmConf;
using static IOB_UT_NEXT.CustomObj;
using static IOB_UT_NEXT.DataModel.Fimat;
using static MapoSDK.WharehouseData;

namespace IOB_WIN_FORM.Iob
{
    public partial class Generic : BaseObj
    {
        #region Protected Fields

        /// <summary>
        /// Variabile numero errori vari (in lettura) --&gt; se supera soglia maxErroriCheck --&gt; disconnette
        /// </summary>
        protected static int numErroriCheck = 0;

        protected bool _connOk = false;

        /// <summary>
        /// valore booleano di check se sia in fase di COMUNICAZIONE ATTIVA con il PLC/NC
        /// </summary>
        protected bool adpCommAct;

        /// <summary>
        /// porta x adapter (x restart)
        /// </summary>
        protected int adpPortNum;

        /// <summary>
        /// DataOra ultimo avvio adapter x watchdog
        /// </summary>
        protected DateTime adpStartRun;

        /// <summary>
        /// Vettore 32 BIT valori in ingresso al filtro
        /// </summary>
        protected int B_input;

        /// <summary>
        /// Vettore 32 BIT valori in uscita dal filtro
        /// </summary>
        protected int B_output;

        /// <summary>
        /// Vettore 32 BIT valori precedenti
        /// </summary>
        protected int B_previous = -1;

        /// <summary>
        /// Cod grupo IOB x creazione PODL al volo
        /// </summary>
        protected string CodGruppoIob = "ND-00";

        /// <summary>
        /// Num errori check alive
        /// </summary>
        protected int currAliveErrors = 0;

        /// <summary>
        /// Dizionario valori impostati x produzione
        /// </summary>
        protected Dictionary<string, string> currProdData = new Dictionary<string, string>();

        /// <summary>
        /// num corrente errori read PLC
        /// </summary>
        protected int currReadErrors = 0;

        /// <summary>
        /// num errori send
        /// </summary>
        protected int currSendErrors = 0;

        /// <summary>
        /// Tempo di attesa in minuti x lettura contapezzi standard (da .ini / OptPar)
        /// </summary>
        protected double delayMinReadPzCount = 0;

        /// <summary>
        /// Fattore di demoltiplicazione dei DynData x ridurre campionamento
        /// </summary>
        protected int demFactDynData = 1;

        /// <summary>
        /// Boolean x indicare contapezzi disabilitato forzatamente da IOB
        /// </summary>
        protected bool disablePzCountByIob = false;

        /// <summary>
        /// Veto x esecuzione task AutoDossier
        /// </summary>
        protected DateTime dtVetoAutoDossier = DateTime.Now;

        /// <summary>
        /// Veto x esecuzione Task2Exe troppo frequenti
        /// </summary>
        protected DateTime dtVetoTask2Exe = DateTime.Now;

        /// <summary>
        /// Veto x lettura contapezzi (in caso di autoODL con attesa reset ad esempio...)
        /// </summary>
        protected DateTime dtVetoReadPzCount = DateTime.Now;

        /// <summary>
        /// Determina se sia prevista gestione PODL (creazione/avvio/chiusura) come Soitaab
        /// </summary>
        protected bool EnabelPodlManFull = false;

        /// <summary>
        /// Abilita riscrittura memoria se trova differenze lettura/richiesti
        /// </summary>
        protected bool ENABLE_MEM_REWRITE = true;

        /// <summary>
        /// Abilitazione restart (da opt par...)
        /// </summary>
        protected bool enableCliRestart = false;

        /// <summary>
        /// Abilitazione invio dataitem
        /// </summary>
        protected bool enableSendDataItem = true;
        protected int minVetoSendDataItem = 60;
        /// <summary>
        /// DataOra x veto all'invio dataItem
        /// </summary>
        protected DateTime dtVetoSenDataItem = DateTime.Now;

        /// <summary>
        /// Boolean x indicare contapezzi abilitato a livello di conf applicazione
        /// </summary>
        protected bool enablePzCountByApp = true;

        /// <summary>
        /// Abilitazione gestione slow data
        /// </summary>
        protected bool enableSlowData = false;

        /// <summary>
        /// dizionario (opzionale) xz decodifica file da importare
        /// </summary>
        protected Dictionary<string, int> FileDecod = new Dictionary<string, int>();

        /// <summary>
        /// DeadBand x riduzione dati FluxLog (se 0 non gestita)
        /// </summary>
        protected double fluxLogRedDeadBand = 0;

        /// <summary>
        /// Determina se sia gestita riduzione dati FluxLog
        /// </summary>
        protected bool fluxLogReduce = false;

        /// <summary>
        /// Dizionario dei valori FluxLog ultimi verificati x veto
        /// </summary>
        protected Dictionary<string, double> fluxLogReduceLast = new Dictionary<string, double>();

        /// <summary>
        /// Dizionario dei valori FluxLog ultimi tipo STRING verificati x veto
        /// </summary>
        protected Dictionary<string, string> fluxLogReduceLastString = new Dictionary<string, string>();

        /// <summary>
        /// Dizionario dei veto send x ogni variabile quando non variata
        /// </summary>
        protected Dictionary<string, DateTime> fluxLogReduceVeto = new Dictionary<string, DateTime>();

        /// <summary>
        /// Finestra in minuti x invio dati FluxLog quando invariati
        /// </summary>
        protected int fluxLogResendPeriod = 60;

        /// <summary>
        /// indica che è richiesto forzatamente reset contapezzi
        /// </summary>
        protected bool forcePzReset = false;

        /// <summary>
        /// indica che è richiesto forzatamente reset contapezzi
        /// </summary>
        protected DateTime forcePzResetUntil = DateTime.Now.AddMinutes(-1);

        /// <summary>
        /// DEADBAND globale se impostata (es x gestire variazioni minime valori FluxLog da inviare...)
        /// </summary>
        protected float GLOBAL_DBAND = 0;

        /// <summary>
        /// Determina se siano gestite le ricette
        /// </summary>
        protected bool hasRecipe = false;

        /// <summary>
        /// Array dei contatori x segnali blinking
        /// </summary>
        protected int[] i_counters;

        /// <summary>
        /// Indica impianto IN SETUP (fino a quando SMETTE di esserlo...)
        /// </summary>
        protected bool inSetup = false;

        /// <summary>
        /// ultimo tentativo connessione...
        /// </summary>
        protected DateTime lastConnectTry;

        /// <summary>
        /// Dizionario ULTIMI valori impostati x produzione
        /// </summary>
        protected Dictionary<string, string> lastProdData = new Dictionary<string, string>();

        /// <summary>
        /// Ultimo invio contapezzi (x invio delayed)
        /// </summary>
        protected DateTime lastPzCountSend;

        /// <summary>
        /// Dizionario ultimi valori (string) delle TSS
        /// </summary>
        protected Dictionary<string, string> LastTSS = new Dictionary<string, string>();

        /// <summary>
        /// Dizionario ultimi valori (string) delle TSS
        /// </summary>
        protected Dictionary<string, DateTime> LastTSSSend = new Dictionary<string, DateTime>();

        /// <summary>
        /// Dizionario ultimi valori (double) delle TSVC
        /// </summary>
        protected Dictionary<string, double> LastTSVC = new Dictionary<string, double>();

        /// <summary>
        /// Ultima registrazione warning x ODL mancante (x scrivere solo ogni 15 secondi)
        /// </summary>
        protected DateTime lastWarnODL;

        /// <summary>
        /// ultima data-ora invio parametri write x ribadire status
        /// </summary>
        protected DateTime lastWriteParamsUpsert = DateTime.Now;

        /// <summary>
        /// Separatore linea (tipicamente x commenti)
        /// </summary>
        protected string lineSep = "--------------------------";

        /// <summary>
        /// Elenco parametri calcolati da inviare alla macchina (es ricetta con traduzione, RAMA/TFT)
        /// </summary>
        protected List<objItem> list2Write = new List<objItem>();

        /// <summary>
        /// Elenco delle eventuali condizioni di veto conditions attive
        /// </summary>
        protected List<string> ListVetoCond = new List<string>();

        /// <summary>
        /// Num massimo di errori in funzionalità check alive
        /// </summary>
        protected int maxAliveErrors = utils.CRI("maxAliveErrors");

        /// <summary>
        /// Soglia massima errori prima della disconnessione automatica in check
        /// </summary>
        protected int maxErroriCheck = utils.CRI("maxErroriCheck");

        /// <summary>
        /// Quantità massima per singola ricetta (per determinare num ricette da inviare)
        /// </summary>
        protected int maxQtyPerFile = 0;

        /// <summary>
        /// Num massimo di errori di rete read (dal PLC)
        /// </summary>
        protected int maxReadErrors = utils.CRI("maxReadErrors");

        /// <summary>
        /// Periodo massimo (in sec) per letture dati in mancanza di eventi di aggiornamento
        /// </summary>
        protected int MaxSecReload = 120;

        /// <summary>
        /// Num massimodi errori di rete send al server
        /// </summary>
        protected int maxSendErrors = utils.CRI("maxSendErrors");

        /// <summary>
        /// Numero massimo di tentativi x test ping preliminare
        /// </summary>
        protected int maxTryPing = 1;

        protected string mem2trace = "";

        /// <summary>
        /// indica se serva refresh parametri e quindi PLC...
        /// </summary>
        protected bool needRefresh = true;

        /// <summary>
        /// Indica se usare la parte numerica di un articolo come codice INT
        /// </summary>
        protected bool numArtCharTrim = false;

        /// <summary>
        /// Timeout x ping al server
        /// </summary>
        protected int pingServerMsTimeout = utils.CRI("PingMsTimeout");

        /// <summary>
        /// DataOra avvio Programma x check avvio
        /// </summary>
        protected DateTime prgStarted = DateTime.Now;

        /// <summary>
        /// Ritardo minimo x invio contapezzi
        /// </summary>
        protected int pzCountDelay = 2500;

        /// <summary>
        /// Periodo di campionamento x refresh info
        /// </summary>
        protected int samplePeriod = 1000;

        /// <summary>
        /// MsSample Period base x campionamenti tpo OCP-UA
        /// </summary>
        protected int samplePeriodBase = 600;

        /// <summary>
        /// Dizionario di VC da trattare come TimeSeries (con conf decodificata + processing successivo...)
        /// </summary>
        protected Dictionary<string, VCData> TSVC_Data = new Dictionary<string, VCData>();

        /// <summary>
        /// Indica se usare archivio locale ricette vs scarico http/REST
        /// </summary>
        protected bool useLocalRecipe = true;

        /// <summary>
        /// Dizionario di VARIABILI da trattare come eventi (da inviare quando cambiano oppure a
        /// scadenza periodo...)
        /// </summary>
        protected Dictionary<string, EVData> VarArray = new Dictionary<string, EVData>();

        /// <summary>
        /// Dizionario dei divieti di invio x ogni counter gestito
        /// </summary>
        protected Dictionary<string, DateTime> VetoCounterSend = new Dictionary<string, DateTime>();

        /// <summary>
        /// Durata in secondi del divieto accodamento segnali IN alla fase di startup
        /// </summary>
        protected int vetoQueueIn = 10;

        /// <summary>
        /// Periodo di veto prima di invio nuova conferma dei valori write correnti, default ogni 5 min
        /// </summary>
        protected double vetoSendWriteUpsert = 1;

        /// <summary>
        /// Periodo wathdog di default (2 sec se non specificato)
        /// </summary>
        protected int watchDogPeriod = 2;

        #endregion Protected Fields

        #region Protected Properties

        /// <summary>
        /// Valore del num max invii consecutivi da coda...
        /// </summary>
        protected static int nMaxSend
        {
            get
            {
                int answ = 5;
                try
                {
                    answ = utils.CRI("nMaxSend");
                }
                catch
                { }
                return answ;
            }
        }

        /// <summary>
        /// Indica il counter della keyReq richiesta attiva
        /// - gestito tramite Redis
        /// - a scadenza 25h
        /// - incrementato ogni invio di auto ODL
        /// </summary>
        protected int countKeyRichiesta
        {
            get
            {
                // calcolo keyReq odierna
                int answ = redisMan.getKReqCount(dailyKey);
                return answ;
            }
            set
            {
                // calcolo keyReq odierna
                redisMan.setKReqCount(dailyKey, value);
            }
        }

        protected string dailyKey
        {
            get
            {
                return DateTime.Today.ToString("yyMMdd");
            }
        }

        /// <summary>
        /// Dizionario delle deadband attive
        /// </summary>
        protected Dictionary<string, float> dictActDBand { get; set; } = new Dictionary<string, float>();

        protected Dictionary<string, string> DictNumArt { get; set; } = new Dictionary<string, string>();

        protected bool disDynDataRangeCheck { get; set; } = false;

        /// <summary>
        /// Folder in cui è presente il tool import excel + file json di conf
        /// </summary>
        protected string exclToolDirPath { get; set; } = "";

        /// <summary>
        /// Folder in cui salvare i file importati (es excel)
        /// </summary>
        protected string fileArchiveFolder { get; set; } = "";

        /// <summary>
        /// Folder da cui importare i file (es excel)
        /// </summary>
        protected string fileImportFolder { get; set; } = "";

        /// <summary>
        /// Tipologia di files da importare (default excel)
        /// </summary>
        protected string fileImportType { get; set; } = "*.xslx";

        /// <summary>
        /// Dati fluxLog serializzati in redis
        /// </summary>
        protected List<GenLogRow> fluxLogData
        {
            get
            {
                List<GenLogRow> answ = new List<GenLogRow>();
                string redKeyFLog = redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:LogFile:FluxLog");
                var rawData = redisMan.getRSV(redKeyFLog);
                if (!string.IsNullOrEmpty(rawData))
                {
                    answ = JsonConvert.DeserializeObject<List<GenLogRow>>(rawData);
                }
                return answ;
            }
            set
            {
                string redKeyFLog = redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:LogFile:FluxLog");
                string fluxLogRaw = JsonConvert.SerializeObject(value);
                redisMan.setRSV(redKeyFLog, fluxLogRaw);
            }
        }

        /// <summary>
        /// Variabile di appoggio GLOBALE x indicare che si può forzare reset contapezzi con
        /// macchina in RUN
        /// </summary>
        protected bool forceResetInRun { get; set; } = false;

        /// <summary>
        /// Variabile di appoggio GLOBALE x indicare macchina RUN (es x gestione su reset pezzi)
        /// </summary>
        protected bool isRunning { get; set; } = false;

        /// <summary>
        /// Variabile isRunning (NON realtime)
        /// </summary>
        protected bool isRunningState
        {
            get
            {
                bool answ = false;
                string cStatus = redisMan.getRSV(redKeyProdRunState);
                bool.TryParse(cStatus, out answ);
                return answ;
            }
            set
            {
                redisMan.setRSV(redKeyProdRunState, $"{value}");
            }
        }

        protected string lastArtDescr
        {
            get => redisMan.getRSV(redKeyProdLastArt);
            set => redisMan.setRSV(redKeyProdLastArt, value);
        }

        /// <summary>
        /// Wrapper di log
        /// </summary>
        protected override Logger lg
        {
            get => _logger;
        }

        /// <summary>
        /// Elenco Articoli (x invio verso macchina)
        /// </summary>
        protected List<ArtRow> ListaArticoli { get; set; } = new List<ArtRow>();

        /// <summary>
        /// Elenco Job di produzione (x invio verso macchina)
        /// </summary>
        protected List<JobRow> ListaJobs { get; set; } = new List<JobRow>();

        /// <summary>
        /// Valore limite MASSIMO di invio di dati come array Json
        /// </summary>
        protected int maxJsonData { get; set; } = utils.CRI("maxJsonData");

        /// <summary>
        /// Valore limite MASSIMO di invio di dati come array Json x EVENTI
        /// </summary>
        protected int maxJsonDataEv { get; set; } = utils.CRI("maxJsonDataEv");

        /// <summary>
        /// Max tentativi ping permessi (default: 5)
        /// </summary>
        protected int maxPingRetry { get; set; } = 5;

        /// <summary>
        /// Coda massima ammessa per FLog (se ‹= 0 disattivata...)
        /// </summary>
        protected int maxQueueFLog { get; set; } = utils.CRI("maxQueueFLog");

        /// <summary>
        /// Coda massima ammessa per FLog (se ‹= 0 disattivata...)
        /// </summary>
        protected int maxQueueRawTransf { get; set; } = utils.CRI("maxQueueRawTransf");

        /// <summary>
        /// Valore MINIMO limite x decidere invio di dati come array Json
        /// </summary>
        protected int minJsonData { get; set; } = utils.CRI("minJsonData");

        protected string nextKeyRich
        {
            get
            {
                return $"{dailyKey}{countKeyRichiesta:00}";
            }
        }

        /// <summary>
        /// Numero letture IN da avvio
        /// </summary>
        protected int nReadFilt { get; set; }

        /// <summary>
        /// Numero letture IN da avvio
        /// </summary>
        protected int nReadIN { get; set; }

        /// <summary>
        /// Numero invii OUT (svuotamento coda)
        /// </summary>
        protected int nSendOut { get; set; }

        /// <summary>
        /// Numero simulazioni ammesse...
        /// </summary>
        protected int numSim { get; set; }

        /// <summary>
        /// Dizionario condizioni di check BIT da usare x valori controllo (es ModBus TCP Imax)
        /// </summary>
        protected Dictionary<string, BitConditionCheck> OptCheckCondBit { get; set; } = new Dictionary<string, BitConditionCheck>();

        /// <summary>
        /// Dizionario condizioni di check INT da usare x valori controllo bitmap (es ModBus TCP Zetapack)
        /// </summary>
        protected Dictionary<string, IntConditionCheck> OptCheckCondInt { get; set; } = new Dictionary<string, IntConditionCheck>();

        /// <summary>
        /// Elenco di valori da tradurre da valori BIT (es ModBus TCP FIMAT)
        /// NB: potrebbero essere contemporaneamente attivi + valori e + traduzioni
        /// </summary>
        protected Dictionary<string, string> OptVar2TranslBit { get; set; } = new Dictionary<string, string>();

        /// <summary>
        /// Elenco di valori da tradurre da valori INT esclusivi (es ModBus TCP FIMAT)
        /// </summary>
        protected Dictionary<string, string> OptVar2TranslInt { get; set; } = new Dictionary<string, string>();

        /// <summary>
        /// Elenco dei fullPath utili x processing
        /// </summary>
        protected Dictionary<string, string> pathList { get; set; } = new Dictionary<string, string>();

        /// <summary>
        /// Gestione archivio serializzato PODL inviati (tramite file temp locale/REDIS)
        /// </summary>
        protected Dictionary<int, PODLModel> POdlSentFileArch
        {
            get
            {
                Dictionary<int, PODLModel> answ = new Dictionary<int, PODLModel>();
                string redKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:POdlSent");
                string rawData = redisMan.getRSV(redKey);
                if (!string.IsNullOrEmpty(rawData))
                {
                    try
                    {
                        answ = JsonConvert.DeserializeObject<Dictionary<int, PODLModel>>(rawData);
                        lgInfo($"Rilettura status POdlSentFileArch: trovati {answ.Count} record");
                    }
                    catch (Exception exc)
                    {
                        lgError($"Errore in deserializzazione POdlSentFileArch{Environment.NewLine}{exc}");
                    }
                }
                return answ;
            }
            set
            {
                string rawVal = JsonConvert.SerializeObject(value);
                string redKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:POdlSent");
                redisMan.setRSV(redKey, rawVal);
                lgDebug($"Salvataggio status POdlSentFileArch | {value.Count} record");
            }
        }

        /// <summary>
        /// Elenco corrente (in memory) PODL inviati all'impianto
        /// chiave: idxPODL
        /// valore: record PODL
        /// </summary>
        protected Dictionary<int, PODLModel> POdlSentList { get; set; } = new Dictionary<int, PODLModel>();

        /// <summary>
        /// Indica se sia stato resettato un contapezzi
        /// </summary>
        protected bool pzCountResetted { get; set; } = false;

        /// <summary>
        /// Fornisce il valore letto da BITMAP in formato valido x messa in coda nel formato dtEve#valReq#cont
        /// </summary>
        protected string qEncodeIN
        {
            get
            {
                string answ = "";
                try
                {
                    answ = string.Format("{0:yyyyMMddHHmmssfff}#{1:X2}#{2}", DateTime.Now, B_output, counterSigIN);
                }
                catch
                { }
                return answ;
            }
        }

        /// <summary>
        /// Definizioni x replace in file ricette
        /// </summary>
        protected Dictionary<string, string> RecipeReplRules { get; set; } = new Dictionary<string, string>();

        /// <summary>
        /// Chiave ultima condition registrata redis (da aggiungere eventuale ID specifico condition)
        /// </summary>
        protected string redKeyLastCondition
        {
            get => redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:Condition");
        }
        protected string redKeyLogfileAct
        {
            get => redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:LogFile:Act");
        }

        protected string redKeyLogfileLast
        {
            get => redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:LogFile:Last");
        }

        protected string redKeyProdLastArt
        {
            get => redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:prodDecod:lastArtDesc");
        }

        protected string redKeyProdRunState
        {
            get => redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.FilenameIOB}:prodDecod:isRunningState");
        }

        /// <summary>
        /// Redis key del dizionari valori DataItemMem persistiti
        /// </summary>
        protected string rKeyFluxMem
        {
            get => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:FluxMem");
        }

        protected Random rndGen { get; set; } = new Random();

        /// <summary>
        /// Indica se vada inviata la keyReq richiesta con lo split/autoODL
        /// </summary>
        protected bool sendKeyRichiesta { get; set; } = false;

        /// <summary>
        /// test ping all'indirizzo PLC/CNC impostato nei parametri
        /// </summary>
        /// <returns></returns>
        protected IPStatus testPingMachine
        {
            get
            {
                IPStatus answ = IPStatus.Unknown;
                // se disabilitato salto...
                if (IOBConfFull.Device.DisabPing)
                {
                    answ = IPStatus.Success;
                }
                else
                {
                    IPAddress address;
                    PingReply reply;
                    using (Ping pingSender = new Ping())
                    {
                        address = IPAddress.Loopback;
                        int pingMsTimeout = IOBConfFull.Device.Connect.PingMsTimeout;
                        IPAddress.TryParse(IOBConfFull.Device.Connect.PingIpAddr, out address);
                        try
                        {
                            // se != null --> uso address...
                            if (address != null)
                            {
                                reply = pingSender.Send(address, pingMsTimeout);
                            }
                            else
                            {
                                reply = pingSender.Send(IOBConfFull.Device.Connect.IpAddr, pingMsTimeout);
                            }
                        }
                        catch
                        {
                            reply = pingSender.Send(IPAddress.Loopback, pingMsTimeout);
                        }
                        answ = reply.Status;
                    }
                }
                return answ;
            }
        }

        /// <summary>
        /// URL per INVIO IN BLOCCO di un INCREMENTO x contapezzi (quelli della macchina: PLC/CNC)...
        /// </summary>
        protected string urlAddPzCount
        {
            get => $@"{urlCommandIob("savePzCountInc")}?qty=";
        }

        /// <summary>
        /// URL per INVIO IN BLOCCO di un INCREMENTO x contapezzi (quelli della macchina: PLC/CNC)
        /// in una data SPECIFICA
        /// </summary>
        protected string urlAddPzCountAtDate
        {
            get => $@"{urlCommandIob("savePzCountIncAtDate")}?qty=";
        }

        /// <summary>
        /// URL per check alive...
        /// </summary>
        protected string urlAlive
        {
            get
            {
                return $@"{urlCommand("alive")}";
            }
        }

        /// <summary>
        /// URL per creazione di un PODL da cod art (metodo GET)...
        /// </summary>
        protected string urlCreatePOdl
        {
            get => $@"{urlCommandIob("forceCreatePOdl")}?";
        }

        /// <summary>
        /// URL per chiamata generazione Snapshot Dossier giornalieri alla data
        /// </summary>
        protected string urlFixDailyDossier
        {
            get => $@"{urlCommandIob("fixDailyDossier")}";
        }

        /// <summary>
        /// URL per chiamata generazione ODL giornalieri alla data
        /// </summary>
        protected string urlFixDailyOdl
        {
            get => $@"{urlCommandIob("fixDailyOdl")}";
        }

        /// <summary>
        /// URL per chiamata generazione ODL giornalieri alla data + conferma PzCount ad ogni step
        /// </summary>
        protected string urlFixDailyOdlConfPzCount
        {
            get => $@"{urlCommandIob("fixDailyOdlConfPzCount")}";
        }

        /// <summary>
        /// URL per forzare split ODL...
        /// </summary>
        protected string urlForceSplit
        {
            get => $"{urlCommandIob("forceSplitOdlFull")}?doConfirm=true&qtyFromLast=true&roundStep=500";
        }

        /// <summary>
        /// URL per recupero PODL NEXT = ATTIVABILI x macchina...
        /// </summary>
        protected string urlGetActPODL
        {
            get => $@"{urlCommandIob("getPOdlAct")}";
        }

        /// <summary>
        /// URL per recupero ARTICOLI correnti x macchina...
        /// </summary>
        protected string urlGetCurrArt
        {
            get => $@"{urlCommandIob("getLastArtByMacc")}";
        }

        /// <summary>
        /// URL per recupero DOSS correnti x macchina...
        /// </summary>
        protected string urlGetCurrDOSS
        {
            get => $@"{urlCommandIob("getLastDossByMacc")}";
        }

        /// <summary>
        /// URL per recupero IDX ODL corrente...
        /// </summary>
        protected string urlGetCurrODL
        {
            get => $@"{urlCommandIob("getCurrODL")}";
        }

        /// <summary>
        /// URL per recupero dati ODL corrente...
        /// </summary>
        protected string urlGetCurrOdlRow
        {
            get => $@"{urlCommandIob("getCurrOdlRow")}";
        }

        /// <summary>
        /// URL per recupero PODL ATTIVABILI (quindi correnti in senso di pronti) x macchina...
        /// </summary>
        [Obsolete("Metodo obsoleto (dubbia interpretazione): sostituire con urlGetNextPODL se si vogliono PROSSIMI POdl attivabili (significato originale) o con urlGetActPODL x il POdl attualmente in produzione (per casi come Rama OPC-UA Siemens)")]
        protected string urlGetCurrPODL
        {
            get => $@"{urlCommandIob("getCurrPODL")}";
        }

        /// <summary>
        /// URL per recupero ListValue tipo fasi...
        /// </summary>
        protected string urlGetListValFasiPodl
        {
            get => $@"{urlCommand("getListValByTable")}PODL";
        }

        /// <summary>
        /// URL per recupero PODL NEXT = ATTIVABILI x macchina...
        /// </summary>
        protected string urlGetNextPODL
        {
            get => $@"{urlCommandIob("getPOdlNext")}";
        }

        /// <summary>
        /// URL per recupero traduzione CodArt in numerico...
        /// </summary>
        protected string urlGetNumArt
        {
            get => $@"{urlCommandIob("getArtNum")}";
        }

        /// <summary>
        /// URL per recupero traduzione CodComm in numerico...
        /// </summary>
        protected string urlGetNumComm
        {
            get => $@"{urlCommandIob("getXdlNum")}";
        }

        /// <summary>
        /// URL per recupero num pezzi ODL corrente...
        /// </summary>
        protected string urlGetNumPzCurrODL
        {
            get => $@"{urlCommandIob("getCurrOdlQtaReq")}";
        }

        /// <summary>
        /// URL per richiamo parametri da scrivere...
        /// </summary>
        protected string urlGetParams2Write
        {
            get => $@"{urlCommandIob("getObjItems2Write")}";
        }

        /// <summary>
        /// URL per recupero contapezzi...
        /// </summary>
        protected string urlGetPzCount
        {
            get => $@"{urlCommandIob("getCounter")}";
        }

        /// <summary>
        /// URL per recupero contapezzi REGISTRATI da TC...
        /// </summary>
        protected string urlGetPzCountRec
        {
            get => $@"{urlCommandIob("getCounterTCRec")}";
        }

        /// <summary>
        /// URL per richiamo task da eseguire...
        /// </summary>
        protected string urlGetTask2Exe
        {
            get => $@"{urlCommandIob("getTask2Exe")}";
        }

        /// <summary>
        /// URL per recupero idle time IOB...
        /// </summary>
        protected string urlIdleTime
        {
            get => $@"{urlCommandIob("getIdlePeriod")}";
        }

        /// <summary>
        /// URL per recupero inizio ODL...
        /// </summary>
        protected string urlInizioOdlIob
        {
            get => $@"{urlCommandIob("getCurrOdlStart")}";
        }

        /// <summary>
        /// URL per check se abilitato...
        /// </summary>
        protected string urlIobEnabled
        {
            get => $@"{urlCommandIob("enabled")}";
        }

        /// <summary>
        /// URL per richiesta chiusura manuale ad utente x ODL corrente (metodo GET)...
        /// </summary>
        protected string urlODLAskClose
        {
            get => $@"{urlCommandIob("askCloseODL")}?idxOdl=";
        }

        /// <summary>
        /// URL per chiusura ODL corrente (metodo GET)...
        /// </summary>
        protected string urlODLClose
        {
            get => $@"{urlCommandIob("closeODL")}/?idxOdl=";
        }

        /// <summary>
        /// URL per AVVIO di un ODL da PODL indicato (metodo GET)...
        /// </summary>
        protected string urlOdlStartFromPOdl
        {
            get => $@"{urlCommandIob("forceStartPOdl")}?idxPODL=";
        }

        /// <summary>
        /// URL per chiusura PODL --&gt; ODL corrente (metodo GET)...
        /// </summary>
        protected string urlPODLClose
        {
            get => $@"{urlCommandIob("closePODL")}?idxPOdl=";
        }

        /// <summary>
        /// URL per richiamo task da eseguire...
        /// </summary>
        protected string urlRemTask2Exe
        {
            get => $@"{urlCommandIob("remTask2Exe")}?taskName=";
        }

        /// <summary>
        /// URL per salvataggio dati PARAMETRI IOB...
        /// </summary>
        protected string urlSaveAllParams
        {
            get => $@"{urlCommandIob("setObjItems")}";
        }

        /// <summary>
        /// URL x salvataggio elenco dataItems OpcUa/MTC
        /// </summary>
        protected string urlSaveDataItems
        {
            get => $@"{urlCommandIob("saveDataItems")}";
        }

        /// <summary>
        /// URL per invio MachineIobConf info
        /// </summary>
        protected string urlSaveMachIobConf
        {
            get => $@"{urlCommandIobFile("saveMachineIobConf")}";
        }

        /// <summary>
        /// URL per salvataggio dati conf memoria IOB...
        /// </summary>
        protected string urlSaveMemMap
        {
            get => $@"{urlCommandIobFile("saveConf")}";
        }

        /// <summary>
        /// URL per INVIO di un update dello status di un certo allarme
        /// </summary>
        protected string urlSendAlarm
        {
            get => $@"{urlCommandIob("sendAlarmBankUpdate")}";
        }

        /// <summary>
        /// URL per invio info HASH gestione recipes
        /// </summary>
        protected string urlSetHashDict
        {
            get => $@"{urlCommandIob("setRedisHashDict")}";
        }

        /// <summary>
        /// URL per salvataggio dati associazione Machine 2 IOB...
        /// </summary>
        protected string urlSetM2IOB
        {
            get => $@"{urlCommandIobFile("setM2IOB")}?IOB_name={Environment.MachineName}";
        }

        /// <summary>
        /// URL per salvataggio VALORI opzionali...
        /// </summary>
        protected string urlSetOptVal
        {
            get => $@"{urlCommandIob("addOptPar")}?";
        }

        /// <summary>
        /// URL per salvataggio contapezzi...
        /// </summary>
        protected string urlSetPzCount
        {
            get => $@"{urlCommandIob("setCounter")}?counter=";
        }

        /// <summary>
        /// URL per effettuare salvataggio parametri (snapshot) macchina...
        /// </summary>
        protected string urlTakeSnapshot
        {
            get => $@"{urlCommandIob("takeFlogSnapshot")}";
        }

        /// <summary>
        /// URL per salvataggio in UPSERT dei PARAMETRI IOB scritti...
        /// </summary>
        protected string urlUpdateWriteParams
        {
            get => $@"{urlCommandIob("upsertObjItems")}";
        }

        /// <summary>
        /// Secondi standard x veto check status e log
        /// </summary>
        protected int vetoSeconds { get; set; } = utils.CRI("vetoSeconds");

        #endregion Protected Properties

        #region Protected Methods

        /// <summary>
        /// Decodifica file MAP (caso <paramref name="ByteNum" />.bit)
        /// </summary>
        /// <param name="linea"></param>
        /// <param name="separator"></param>
        /// <param name="ByteNum">indirizzo Byte: indirizzo di partenza memoria</param>
        /// <param name="memSize">dimensione singolo slot in byte</param>
        /// <param name="BitNum">indirizzo bit: numero riga x calcolo indice bit</param>
        /// <returns></returns>
        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;
            }
        }

        /// <summary>
        /// Decodifica file MAP generico
        /// </summary>
        /// <param name="linea"></param>
        /// <param name="separator"></param>
        /// <param name="memPre"></param>
        /// <param name="baseAddr"></param>
        /// <param name="memSize"></param>
        /// <returns></returns>
        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;
        }

        /// <summary>
        /// Decodifica valore della coda IN nel formato sendEnab[0]=dtEve sendEnab[1]=valore sendEnab[2]=counter
        /// </summary>
        /// <param name="queueVal">dtEve + '#' + valReq + '#' + cont</param>
        /// <returns></returns>
        protected static string[] qDecodeIN(string queueVal)
        {
            string[] answ = null;
            if (!string.IsNullOrEmpty(queueVal))
            {
                try
                {
                    answ = queueVal.Split('#');
                }
                catch
                { }
            }
            return answ;
        }

        /// <summary>
        /// Accodamento richieste server
        /// </summary>
        /// <param name="codTav"></param>
        /// <param name="newReq"></param>
        protected void accodaServReq(string codTav, string newReq)
        {
            JobTaskData jobTask = new JobTaskData(codTav, newReq);
            // accodo richiesta serializzata
            string serVal = JsonConvert.SerializeObject(jobTask);
            QueueSrvReq.Enqueue(serVal);
            // loggo!
            lgDebug($"[QUEUE] | QueueSrvReq len: {QueueSrvReq.Count}");
        }

        /// <summary>
        /// Accodamento risposte per il server
        /// </summary>
        /// <param name="codTav"></param>
        /// <param name="rawData"></param>
        protected void accodaServResp(string codTav, string rawData)
        {
            JobTaskData jobTask = new JobTaskData(codTav, rawData);
            // accodo richiesta serializzata
            string serVal = JsonConvert.SerializeObject(jobTask);
            QueueSrvResp.Enqueue(serVal);
            // loggo!
            lgDebug($"[QUEUE] | QueueSrvResp len: {QueueSrvResp.Count}");
        }

        /// <summary>
        /// Classe di base implementazione traduzione di una LUT da memoria come valore BIT a valore
        /// esplicito x FLog
        /// </summary>
        protected virtual void checkTranslateBit()
        {
            // verifico se devo processare decodifica di qualche valore...
            if (OptVar2TranslBit != null && OptVar2TranslBit.Count > 0)
            { }
        }

        /// <summary>
        /// Classe di base implementazione traduzione di una LUT da memoria come valore INT a valore
        /// esplicito x FLog
        /// </summary>
        protected virtual void checkTranslateInt()
        {
            if (OptVar2TranslInt != null && OptVar2TranslInt.Count > 0)
            { }
        }

        /// <summary>
        /// Verifico i dynData x validità:
        /// - siano almeno 1
        /// - NON siano tutti identici
        /// </summary>
        /// <param name="currDynData"></param>
        /// <returns></returns>
        protected bool checkValidDynData(Dictionary<string, string> currDynData)
        {
            bool answ = false;
            // conto num valori
            int numVal = currDynData.Count;
            bool dataPresent = numVal > 0;
            if (dataPresent)
            {
                // prendo il primo valore..
                string firstVal = currDynData.FirstOrDefault().Value;
                // conto val uguali al primo
                int numEq = currDynData.Where(x => x.Value == firstVal).Count();
                // se solo 1 o almeno 1 è diverso è ok
                answ = numVal == 1 || (numVal > numEq);
            }
            return answ;
        }

        /// <summary>
        /// Conversione string row in log generico
        /// </summary>
        /// <param name="dailyLog"></param>
        /// <returns></returns>
        protected virtual GenLogRow convertToMachineLog(string dailyLog)
        {
            GenLogRow answ = new GenLogRow();
            if (!string.IsNullOrEmpty(dailyLog))
            {
                // preventivamente: doppio ";" --> ";"
                dailyLog = dailyLog.Replace(";", ";");
                var sSplit = dailyLog.Split(';');
                answ.dtRif = DateTime.ParseExact($"{sSplit[0]} {sSplit[1]}", "yyyy-M-d HH:mm:ss", null);
                answ.valString = $"{sSplit[2]};{sSplit[3]}";
            }
            return answ;
        }

        /// <summary>
        /// Restituisce stato allarmi in formato byte[]
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        protected byte[] currAlarmsState(BaseAlarmConf item)
        {
            byte[] answ = new byte[item.size];
            int currStatus = 0;
            int[] listInt = new int[2];
            // banchi in array Int16 --> scompongo
            for (int i = 0; i < item.size / 2; i++)
            {
                currStatus = getAlarmStatus(item);

                // aggiornamento blink counters dato nuovo valore
                item.checkBlinkCounter(i, (uint)currStatus);

                // calcolo indice errori
                if ((uint)(item.alarmsMask[i] & currStatus) > 0)
                {
                    var currByte = BitConverter.GetBytes(currStatus);
                    // salvo nell'array di byte
                    Buffer.BlockCopy(currByte, 0, answ, i * 2, 2);
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera ODL attivo su impianto
        /// </summary>
        protected int CurrOdl()
        {
            int cODL = 0;
            // vieto controllo prima di 5 sec... da configurare?
            if (DateTime.Now.Subtract(vetoCheckOdl).TotalSeconds > IOBConfFull.Odl.VetoCheckOdlSec)
            {
                string sCurrODL = utils.CallUrlGet(urlGetCurrODL);
                int.TryParse(sCurrODL, out cODL);
                currIdxODL = cODL;
                vetoCheckOdl = DateTime.Now;
            }
            else
            {
                cODL = currIdxODL;
            }
            return cODL;
        }

        /// <summary>
        /// Aggiunge o aggiorna nDict ad un Dict
        /// </summary>
        /// <param name="currDict"></param>
        /// <param name="newKey"></param>
        /// <param name="newVal"></param>
        protected void DictUpsert(ref Dictionary<string, string> currDict, string newKey, string newVal)
        {
            if (currDict.ContainsKey(newKey))
            {
                currDict[newKey] = newVal;
            }
            else
            {
                currDict.Add(newKey, newVal);
            }
        }

        /// <summary>
        /// Verifica se salvare (nel log) info dei FluxLog filtrati
        /// - per scadenza valore VetoFlushFiltFL
        /// - per un numero complessivo di eventi superiori a soglia
        /// </summary>
        /// <param name="minCount">numero minimo di eventi necessari al salvataggio (come somma complessiva)</param>
        protected void FiltFluxLogCheckSave(long minCount)
        {
            DateTime adesso = DateTime.Now;
            bool doWrite = VetoFlushFiltFL < adesso;
            // conteggio valori
            if (!doWrite)
            {
                long numFL = DictFiltFLog.Sum(x => x.Value);
                doWrite = numFL > minCount;
            }
            if (doWrite)
            {
                // scrivo log x ogni valore
                string sep = "------------------------------------------------------------";
                _logger.Info(sep);
                _logger.Info("- Paret0 Eventi FluxLog filtrati");
                _logger.Info(sep);
                foreach (var item in DictFiltFLog.OrderByDescending(x => x.Value))
                {
                    // loggo!
                    _logger.Info($"{item.Key}: {item.Value}");
                }
                _logger.Info(sep);
                // reset dizionario
                DictFiltFLog.Clear();
                // nuovo veto a 1h
                VetoFlushFiltFL = adesso.AddHours(1);
            }
        }

        /// <summary>
        /// Imposta eventuali altri valori default
        /// </summary>
        protected void fixDefaultPar()
        {
            // parametro max tentativi PING...
            maxPingRetry = IOBConfFull.General.MaxPingRetry;
            maxErroriCheck = IOBConfFull.General.MaxErroriCheck;
            watchDogPeriod = IOBConfFull.General.WatchDogPeriod;

            // sistemo conf folder acquisizione files
            if (IOBConfFull.Special.FileConf != null)
            {
                fileImportFolder = IOBConfFull.Special.FileConf.ImportFolder;
                fileImportType = IOBConfFull.Special.FileConf.ImportType;
                fileArchiveFolder = IOBConfFull.Special.FileConf.ArchiveFolder;
                exclToolDirPath = IOBConfFull.Special.FileConf.ExcelToolDirPath;
            }
            delayMinReadPzCount = IOBConfFull.General.DelayReadPzCount;
        }

        /// <summary>
        /// Effettua un force kill dato nome processo
        /// </summary>
        /// <param name="nomeProc"></param>
        protected void ForceKillByName(string nomeProc)
        {
            Process[] stillRunningProc = Process.GetProcessesByName(nomeProc);
            if (stillRunningProc != null)
            {
                if (stillRunningProc.Length > 0)
                {
                    foreach (var item in stillRunningProc)
                    {
                        try
                        {
                            Process p = Process.GetProcessById(item.Id);
                            {
                                if (!p.HasExited)
                                {
                                    int closeWaitTime = waitForExitMsec * 8;
                                    // se è MAN --> aspetto + a lungo...
                                    if (nomeProc.Contains("IOB-MAN"))
                                    {
                                        closeWaitTime = closeWaitTime * 4;
                                    }
                                    p.CloseMainWindow();
                                    p.WaitForExit(closeWaitTime);
                                }
                                if (!p.HasExited)
                                {
                                    lg.Error($"Process not Exited, 2nd try p.kill()");
                                    p.Kill();
                                    p.WaitForExit(waitForExitMsec);
                                }
                                if (!p.HasExited)
                                {
                                    lg.Error($"Process not Killed, 3nd try p.kill()");
                                    p.Kill();
                                    p.WaitForExit(waitForExitMsec * 2);
                                }
                                if (!p.HasExited)
                                {
                                    lg.Error($"Process not Killed, 4th try p.kill()");
                                    p.Kill();
                                }
                            }
                        }
                        catch (Exception exc)
                        {
                            lg.Error($"Errore in fase di kill processo da nome {exc}");
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Imposta la memoria PLC in modo forzato (es x oggetti gestiti da FTP come setComm)
        /// </summary>
        protected virtual void forceMemMap()
        { }

        /// <summary>
        /// Implementazione di riferimento della verifica stato allarmi
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        protected virtual int getAlarmStatus(BaseAlarmConf item)
        {
            int answ = 0;
            return answ;
        }

        /// <summary>
        /// Implementazione di riferimento della verifica stato allarmi formato UInt
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        protected virtual uint getAlarmStatusUInt(BaseAlarmConf item)
        {
            uint answ = 0;
            return answ;
        }

        /// <summary>
        /// Recupera valore da dizionario CurrProdData o restituisce val default
        /// </summary>
        /// <param name="key">Chiave richiesta</param>
        /// <param name="defVal">Valore di default</param>
        /// <returns></returns>
        protected string getCurrProdData(string key, string defVal)
        {
            string answ = "";
            if (currProdData.ContainsKey(key))
            {
                answ = string.IsNullOrEmpty(currProdData[key]) ? defVal : currProdData[key];
            }
            return answ;
        }

        /// <summary>
        /// Fornisce formato valido x messa in coda nel formato dtEve#valReq#cont
        /// </summary>
        protected string getEncodSigLog(DateTime DtEvent, int val2log, int counter)
        {
            string answ = "";
            try
            {
                answ = $"{DtEvent:yyyyMMddHHmmssfff}#{val2log:X2}#{counter}";
            }
            catch
            { }
            return answ;
        }

        /// <summary>
        /// Recupero dati da FluxLog
        /// </summary>
        /// <param name="resultSet"></param>
        /// <param name="codFlux"></param>
        /// <returns></returns>
        protected string getFluxVal(DossierFluxLogDTO resultSet, string codFlux)
        {
            string answ = "";
            var searchRec = resultSet.ODL.Where(x => x.CodFlux == codFlux).FirstOrDefault();
            if (searchRec != null)
            {
                answ = !string.IsNullOrEmpty(searchRec.ValoreEdit) ? searchRec.ValoreEdit : searchRec.Valore;
            }
            return answ;
        }

        /// <summary>
        /// Recupero dati da FluxLog formato double
        /// </summary>
        /// <param name="resultSet"></param>
        /// <param name="codFlux"></param>
        /// <returns></returns>
        protected double getFluxValDouble(DossierFluxLogDTO resultSet, string codFlux)
        {
            double answ = 0;
            var style = NumberStyles.AllowDecimalPoint;
            var culture = CultureInfo.InvariantCulture;
            string rawVal = getFluxVal(resultSet, codFlux);
            if (rawVal.Contains(","))
            {
                rawVal = rawVal.Replace(",", ".");
            }
            if (!string.IsNullOrEmpty(rawVal))
            {
                double.TryParse(rawVal, style, culture, out answ);
            }
            return answ;
        }

        /// <summary>
        /// Recupero dati da FluxLog formato INT
        /// </summary>
        /// <param name="resultSet"></param>
        /// <param name="codFlux"></param>
        /// <returns></returns>
        protected int getFluxValInt(DossierFluxLogDTO resultSet, string codFlux)
        {
            int answ = 0;
            double dVal = 0;
            var style = NumberStyles.AllowDecimalPoint;
            var culture = CultureInfo.InvariantCulture;
            string rawVal = getFluxVal(resultSet, codFlux);
            if (rawVal.Contains(","))
            {
                rawVal = rawVal.Replace(",", ".");
            }
            if (!string.IsNullOrEmpty(rawVal))
            {
                double.TryParse(rawVal, style, culture, out dVal);
                answ = (int)dVal;
            }
            return answ;
        }

        /// <summary>
        /// Converte un file di log della macchina in un dizionario
        /// </summary>
        /// <param name="dailyLog"></param>
        /// <returns></returns>
        protected List<GenLogRow> getGenLogFromMachineLog(string dailyLog)
        {
            List<GenLogRow> result = new List<GenLogRow>();
            //se ho valori.. faccio split!
            if (!string.IsNullOrEmpty(dailyLog))
            {
                var rowList = dailyLog.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                if (rowList != null && rowList.Length > 0)
                {
                    result = rowList
                                .Where(x => !string.IsNullOrEmpty(x))
                                .Select(x => convertToMachineLog(x))
                                .ToList();
                }
            }
            return result;
        }

        /// <summary>
        /// Effettua traduzione da tabella listValue per chiave
        /// </summary>
        /// <param name="factTable"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        protected string getLV(Dictionary<string, string> currDict, string key)
        {
            string answ = currDict.ContainsKey(key) ? currDict[key] : key;
            return answ;
        }

        /// <summary>
        /// Restituisce valore salvato in memMapWrite
        /// </summary>
        /// <param name="keyName"></param>
        /// <returns></returns>
        protected string getMemMapWriteVal(string keyName)
        {
            string answ = "";
            if (memMap != null && memMap.mMapWrite.ContainsKey(keyName))
            {
                answ = memMap.mMapWrite[keyName].value;
            }
            return answ;
        }

        /// <summary>
        /// Recupera valore numerico ARTICOLO x salvataggio valori INT
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        protected string getNumArt(string value)
        {
            string answ = "";
            // cerco in dizionario corrente...
            if (DictNumArt.Count > 0 && DictNumArt.ContainsKey(value))
            {
                answ = DictNumArt[value];
            }
            // altrimenti chiamo servizio
            else
            {
                // chiamo MP-IO server
                bool srvAlive = CheckServerAlive();
                if (srvAlive)
                {
                    string url2call = $"{urlGetNumArt}?CodArt={value}";
                    if (verboseLog)
                    {
                        lgInfo("chiamata URL " + url2call);
                    }
                    var rawData = utils.CallUrlGet(url2call);
                    // deserializzo e recupero KVP...
                    var dictArtSrv = JsonConvert.DeserializeObject<Dictionary<string, string>>(rawData);
                    // se fosse una chiamata con valore vuoto --> salvo intera tabella...
                    if (string.IsNullOrEmpty(value))
                    {
                        DictNumArt = dictArtSrv;
                    }
                    // altrimenti aggiungo a dizionario
                    else
                    {
                        foreach (var item in dictArtSrv)
                        {
                            if (!DictNumArt.ContainsKey(item.Key))
                            {
                                DictNumArt.Add(item.Key, item.Value);
                            }
                        }
                    }
                    // ora cerco valore e restituisco
                    if (DictNumArt.ContainsKey(value))
                    {
                        answ = DictNumArt[value];
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera valore numerico COMMESSA x salvataggio valori INT
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        protected string getNumComm(string value)
        {
            string answ = "";
            // chiamo MP-IO server
            bool srvAlive = CheckServerAlive();
            if (srvAlive)
            {
                string url2call = $"{urlGetNumComm}?CodXdl={value}";
                if (verboseLog)
                {
                    lgInfo("chiamata URL " + url2call);
                }
                // è direttamente il risultato!
                answ = utils.CallUrlGet(url2call);
            }
            // restituisco
            return answ;
        }

        /// <summary>
        /// Stringa raw dei parametri da scrivere...
        /// </summary>
        /// <returns></returns>
        protected string getParams2write()
        {
            string answ = "";
            string url2call = $"{urlGetParams2Write}";
            if (verboseLog)
            {
                lgInfo("chiamata URL " + url2call);
            }
            answ = utils.CallUrlGet(url2call);
            // se vuoto faccio seconda prova...
            if (string.IsNullOrEmpty(answ) || answ == "[]")
            {
                answ = utils.CallUrlGet(url2call);
            }
            return answ;
        }

        /// <summary>
        /// Recupera file log da analizzare
        /// </summary>
        /// <returns></returns>
        protected virtual bool getRemoteLog()
        {
            bool fatto = false;
            return fatto;
        }

        /// <summary>
        /// Chiede elenco dei task da eseguire
        /// - formato Json
        /// - array di KVP / Dictionary
        /// - formato definito da API x MP/IO/:
        /// - KEY: task
        /// - VALUE: array JSon KVP
        /// </summary>
        /// <param name="codTav">Cod DP/Tavola (opzionale)</param>
        /// <returns></returns>
        protected async Task<string> getTask2exe(string codTav)
        {
            string answ = "";
            bool srvAlive = await CheckServerAliveAsync();
            if (srvAlive)
            {
                string url2call = $"{urlGetTask2Exe}";
                // se ho tavola aggiungo richiesta...
                if (!string.IsNullOrEmpty(codTav))
                {
                    url2call += $"|{codTav}";
                }
                if (verboseLog)
                {
                    lgInfo("chiamata URL " + url2call);
                }
                answ = await utils.callUrlAsync(url2call);
            }
            return answ;
        }

        /// <summary>
        /// Verifica presenza eventuali allarmi
        /// </summary>
        /// <returns></returns>
        protected virtual bool hasAlarms()
        {
            bool answ = false;
            // 2023.12.20: aggiunta gestione secondo tipo allarmi come elenco OPC-UA (BLM/Adige)
            if (alarmType == AlarmBlockType.Bitmap)
            {
                int numRaised = 0;
                int currStatus = 0;
                ushort full16 = 65535;
                int[] listInt = new int[2];
                if (alarmMaps != null)
                {
                    // leggo a ciclo le aree degli allarmi CONFIGURATI, se ne trovo --> segnalo allarme...
                    foreach (var item in alarmMaps)
                    {
                        // banchi in array Int16 --> scompongo
                        for (int i = 0; i < item.size / 2; i++)
                        {
                            currStatus = getAlarmStatus(item);
                            // ora filtro x l'i-esimo banco a 16 bit...
                            if (i == 0)
                            {
                                currStatus = (ushort)(currStatus & full16);
                            }
                            else
                            {
                                var temp = currStatus >> 16;
                                currStatus = (ushort)temp;
                            }

                            // aggiornamento blink counters dato nuovo valore
                            item.checkBlinkCounter(i, (uint)currStatus);

                            // calcolo indice errori
                            if ((uint)(item.alarmsMask[i] & currStatus) > 0)
                            {
                                numRaised++;
                            }
                            // verifico SE sia variato... confronto allarmi filtrato stile blink per
                            // bit status:
                            // - allarmi che iniziano per # IGNORATI
                            // - altri allarmi con un countdown da MAX_COUNTER_BLINK a 0 per il fronte
                            // di discesa
                            if (item.isChanged(i, (uint)currStatus))
                            {
                                lgInfo($"Alarm state change | {item.memAddr} | i: {i} | prev: {item.alarmsState[i]} | curr: {currStatus}");
                                // registro gli allarmi attivi e trasmetto...
                                if (sendAlarmVariations(item.memAddr, i, item.alarmsState[i], (uint)(item.alarmsMask[i] & currStatus), item.messages))
                                {
                                    // se inviato --> salvo stato da current...
                                    item.updStatusVal(i, (uint)(item.alarmsMask[i] & currStatus));
                                    // salvo in redis...
                                    string alarmHash = redisMan.redHash($"IOB:ALARM_STATUS:{IOBConfFull.General.FilenameIOB}:{item.memAddr}");
                                    string rawAlarms = JsonConvert.SerializeObject(item.alarmsState);
                                    redisMan.setRSV(alarmHash, rawAlarms);
                                }
                                else
                                {
                                    lgError($"Errore in sendAlarmVariations | {item.memAddr} | i: {i} | prev: {item.alarmsState[i]} | curr: {currStatus}");
                                }
                            }
                            else
                            {
                                if (currStatus > 0)
                                {
                                    lgTrace($"Alarm UNCHANGED but Active | {item.memAddr} | i: {i} | prev: {item.alarmsState[i]} | curr: {currStatus}");
                                }
                            }
                        }
                    }
                }
                answ = numRaised > 0;
            }
            // gestione allarmi come elenco
            else if (alarmType == AlarmBlockType.ActiveList)
            {
                // verifico SE ho allarmi
                if (alarmMaps != null && alarmMaps.Count > 0)
                {
                    // ciclo x ogni blocco
                    foreach (var item in alarmMaps)
                    {
                        // cerco se sia superiore al livello minimo da conf
                        if (item.blockLevel >= alarmLevelMin)
                        {
                            // controllo variabile counter SE > 0...
                            int numCount = getAlarmStatus(item);
                            answ = numCount > 0;
                        }
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera da server set di dati specifici x IOB
        /// </summary>
        /// <returns></returns>
        protected virtual bool IobGetDataFromServer()
        {
            return false;
        }

        /// <summary>
        /// Effettua upload verso server FTP della macchina dei files nella folder indicata
        /// </summary>
        /// <param name="folderDir"></param>
        /// <returns></returns>
        protected virtual bool iobSendFTP(string folderDir)
        {
            bool answ = false;
            if (IOBConfFull.Special.FtpConf != null)
            {
                // recupero CONF
                var ftpConf = IOBConfFull.Special.FtpConf;
                // procedo SE TROVO conf...
                if (string.IsNullOrEmpty($"{ftpConf.Server}{ftpConf.User}{ftpConf.Passwd}"))
                {
                    lgError($"Impossibile eseguire iobSendFTP x mancanza parametri | ftpServ: {ftpConf.Server} | ftpUser: {ftpConf.User} | ftpPass: {ftpConf.Passwd}");
                }
                else
                {
                    var ftpClient = new Manager(ftpConf.Server, ftpConf.User, ftpConf.Passwd, ftpConf.CertValue, ftpConf.SkipCert);
                    var testServer = ftpClient.ServerOk();
                    if (testServer)
                    {
                        lgInfo($"FTP: server found at {ftpConf.Server}");
                        var srvType = ftpClient.ServerType();
                        lgInfo($"FTP Server type: {srvType}");

                        var preTest = ftpClient.DirExists(ftpConf.DirRemote);
                        if (!preTest)
                        {
                            var dirCreate = ftpClient.CreateDir(ftpConf.DirRemote);
                            lgInfo($"FTP: created remote dir {ftpConf.DirRemote}");
                        }

                        // test directory...
                        string basePath = Application.StartupPath;

                        string localPath = Path.Combine(basePath, ftpConf.DirLocal);
                        lgInfo($"basePath: {basePath} | localPath: {localPath}");
                        var fileList = Directory.GetFiles(localPath, "*.csv");
                        int numfile = 0;
                        foreach (var file in fileList)
                        {
                            string fName = file.Split('\\').Last();
                            string remName = $"{ftpConf.DirRemote}\\{fName}";
                            ftpClient.SendFile(file, remName);
                            numfile++;
                        }
                        var dirUploaded = (numfile == fileList.Count());
                        if (dirUploaded)
                        {
                            lgInfo($"FTP: uploaded dir content {ftpConf.DirLocal} --> {ftpConf.DirRemote}");
                        }
                        // se ok --> sposto invio
                        DateTime adesso = DateTime.Now;
                        string archBasePath = Path.Combine(basePath, "DATA", "HIST", $"{adesso:yyyy}");
                        baseUtils.checkDir(archBasePath);
                        string archPath = Path.Combine(archBasePath, $"{adesso:MMddHHmmss}");
                        baseUtils.checkDir(archPath);
                        lgInfo($"Richiesta Dir Move | {localPath} | {archPath}");
                        foreach (var item in fileList)
                        {
                            string fName = item.Split('\\').Last();
                            string destName = $"{archPath}\\{fName}";
                            File.Move(item, destName);
                        }
                        //Directory.Move(localPath, archBasePath);
                        lgInfo($"FTP: Archived dir content {localPath} --> {archPath}");
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Prepara files CSV da inviare alla macchina
        /// </summary>
        protected virtual bool iobWriteLocalCSV()
        {
            bool answ = false;
            // conf ftp
            if (IOBConfFull.Special.FtpConf != null)
            {
                var ftpConf = IOBConfFull.Special.FtpConf;
                // salvo articoli
                string locDir = ftpConf.DirLocal;
                bool addHeader = ftpConf.CsvAddHeader;
                string basePath = Application.StartupPath;
                string tempDir = Path.Combine(basePath, locDir);
                lgInfo($"iobWriteLocalCSV | locDir: {locDir} | addHeader: {addHeader} | tempDir: {tempDir}");
                baseUtils.checkDir(tempDir);
                string filePath = Path.Combine(tempDir, "articoli.csv");
                answ = DataExport.SaveToCsv(ListaArticoli, filePath, addHeader);
                if (answ)
                {
                    lgInfo($"CSV: saved ART file as articoli.csv at {filePath}");
                    // salvo PODL
                    string csvName = $"{DateTime.Now:dd-MM-yyyy}.csv";
                    filePath = Path.Combine(tempDir, $"{csvName}");
                    answ = DataExport.SaveToCsv(ListaJobs, filePath, addHeader);
                    if (answ)
                    {
                        lgInfo($"CSV: saved PODL file {csvName} at {filePath}");
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Prepara files USTD da inviare alle macchine in formato USTD (Emmegi - taglierine)
        /// </summary>
        protected virtual bool iobWriteLocalUSTD()
        {
            bool answ = false;
#if false
            // conf ftp
            var ftpConf = IOBConfFull.Special.FtpConf;
            // salvo articoli
            string locDir = ftpConf.DirLocal;
            bool addHeader = ftpConf.CsvAddHeader;
            string basePath = Application.StartupPath;
            string tempDir = Path.Combine(basePath, locDir);
            lgInfo($"iobWriteLocalCSV | locDir: {locDir} | addHeader: {addHeader} | tempDir: {tempDir}");
            baseUtils.checkDir(tempDir);
            string filePath = Path.Combine(tempDir, "articoli.csv");
            answ = DataExport.SaveToCsv(ListaArticoli, filePath, addHeader);
            if (answ)
            {
                lgInfo($"CSV: saved ART file as articoli.csv at {filePath}");
                // salvo PODL
                string csvName = $"{DateTime.Now:dd-MM-yyyy}.csv";
                filePath = Path.Combine(tempDir, $"{csvName}");
                answ = DataExport.SaveToCsv(ListaJobs, filePath, addHeader);
                if (answ)
                {
                    lgInfo($"CSV: saved PODL file {csvName} at {filePath}");
                }
            }
#endif
            return answ;
        }

        /// <summary>
        /// Effettua traduzione ITEM da LUT parametrica (keyReq: tipo+id) del file di conf, se non
        /// trovo uso keyReq
        /// </summary>
        /// <param name="tipo"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        protected virtual string itemTranslation(string tipo, string id)
        {
            string answ = "";
            if (IOBConfFull.ItemTranslation != null)
            {
                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 (IOBConfFull.ItemTranslation.ContainsKey(lemma))
                {
                    answ = IOBConfFull.ItemTranslation[lemma];
                }
                else
                {
                    answ = lemma;
                }
            }
            return answ;
        }

        /// <summary>
        /// Effettua traduzione ITEM da LUT parametrica (keyReq: lemma) del file di conf, se non
        /// trovo uso keyReq
        /// </summary>
        /// <param name="lemma"></param>
        /// <returns></returns>
        protected virtual string itemTranslation(string lemma)
        {
            string answ = "";
            if (IOBConfFull.ItemTranslation != null)
            {
                if (IOBConfFull.ItemTranslation.ContainsKey(lemma))
                {
                    answ = IOBConfFull.ItemTranslation[lemma];
                }
                else
                {
                    answ = lemma;
                }
            }
            return answ;
        }

        /// <summary>
        /// Legge il file di conf di una MAP di informazioni da gestire con lettura set memoria
        /// </summary>
        /// <param name="vettoreConf">nome vettore memoria</param>
        /// <param name="nomeFile">file origine</param>
        /// <param name="memSize">dimensione (in byte) della memoria</param>
        /// <param name="numVett">dimensione (in byte) della memoria</param>
        protected void loadConfFile(ref otherData[] vettoreConf, string nomeFile, int memSize, ref int numVett)
        {
            otherData lastData = new otherData();
            int totRighe = 0;
            string linea;
            totRighe = File.ReadLines(nomeFile).Count();
            // creo un vettore della dimensione corretta... conta anche commenti tanto poi riduco...
            vettoreConf = new otherData[File.ReadLines(nomeFile).Count()];
            // carica da file...
            StreamReader file = new StreamReader(nomeFile);
            // leggo 1 linea alla volta...
            int numRiga = 0;
            int bitNum = 0;
            int byteNum = 0;
            while ((linea = file.ReadLine()) != null)
            {
                // SE non è un commento...
                if (linea.Substring(0, 1) != "#")
                {
                    // se finisce per BIT allora processo bit-a-bit...
                    if (linea.EndsWith("BOOL"))
                    {
                        try
                        {
                            string[] memIdx = linea.Split(utils.CRC("testCharSep"))[0].Split('.');
                            // calcolo bit e byte number...
                            int.TryParse(memIdx[0], out byteNum);
                            if (memIdx.Length > 1)
                            {
                                int.TryParse(memIdx[1], out bitNum);
                            }
                            else
                            {
                                bitNum = 0;
                            }
                        }
                        catch
                        {
                            byteNum = 0;
                            bitNum = 0;
                        }
                        lastData = decodeBitData(linea, utils.CRC("testCharSep"), byteNum, 1, bitNum);
                        vettoreConf[numRiga] = lastData;
                    }
                    else
                    {
                        lastData = decodeOtherData(linea, utils.CRC("testCharSep"), "", 1, memSize);
                        vettoreConf[numRiga] = lastData;
                    }
                    numRiga++;
                }
            }
            // salvo lunghezza file...
            try
            {
                numVett = Convert.ToInt32(lastData.memAddr) + 1;
            }
            catch
            {
                numVett = numRiga + 1;
            }
            // chiudo file
            file.Close();
            // ora trimmo vettore al solo numero VERO dei valori caricati...
            Array.Resize<otherData>(ref vettoreConf, numRiga);

            lgInfo(string.Format("Fine caricamento vettore di {0} variabili per file {1}", numRiga, nomeFile));
        }

        /// <summary>
        /// Lettura memorie conf speciali (json) ...SE inserito in [OPTPAR] come PARAM_CONF=nome.json
        /// </summary>
        protected virtual void loadMemConf()
        {
            if (DateTime.Now <= vetoReloadConf)
            {
                lgTrace($"Veto on loadMemConf active until {vetoReloadConf}");
            }
            else
            {
                lgInfo("loadMemConf.01");
                lgInfoStartup("BEGIN loadMemConf");
                // variabili x gestione send contapezzi in blocco
                enableSendPzCountBlock = IOBConfFull.Counters.EnabSendPzcBlock;
                minSendPzCountBlock = IOBConfFull.Counters.SendPzcBlockMin;
                maxSendPzCountBlock = IOBConfFull.Counters.SendPzcBlockMax;
                lgInfo("loadMemConf.02");
                minRespTimeMs = IOBConfFull.Device.MinRespTimeMs;
                // gruppo macchina
                CodGruppoIob = IOBConfFull.General.CodGruppoIob;
                // gestione completa PODL
                EnabelPodlManFull = IOBConfFull.General.EnabelPodlManFull;

                // abilitazione invio WDST disabilitazione controllo range valori DynData
                disableWdst = IOBConfFull.General.DisabWDST;

                // disabilitazione controllo range valori DynData
                disDynDataRangeCheck = IOBConfFull.FluxLog.DisDynDataRangeCheck;
                lgInfo("loadMemConf.03");

                // se ho info in IOBConfFull uso quello...
                if (IOBConfFull.Memory != null)
                {
                    lgInfo("loadMemConf.05a");
                    memMap = IOBConfFull.Memory;
                }
                else
                {
                    lgInfo("loadMemConf.05b");
                    // inizializzo LUT decodifica PARAMETRI
                    string jsonParams = getOptPar("PARAM_CONF");
                    if (!string.IsNullOrEmpty(jsonParams))
                    {
                        string jsonFileName = $"{Application.StartupPath}\\DATA\\CONF\\{jsonParams}";
                        lgInfoStartup($"Apertura file {jsonFileName}");
                        StreamReader reader = new StreamReader(jsonFileName);
                        string jsonData = reader.ReadToEnd();
                        if (!string.IsNullOrEmpty(jsonData))
                        {
                            lgInfoStartup($"File json PARAMETRI composto da {jsonData.Length} caratteri");
                            lgInfo("loadMemConf.04");
                            try
                            {
                                memMap = JsonConvert.DeserializeObject<plcMemMapExt>(jsonData);
                            }
                            catch (Exception exc)
                            {
                                lgError($"Eccezione in decodifica conf json{Environment.NewLine}{exc}");
                            }
                        }
                        else
                        {
                            lgError("Errore in loadMemConf: file json vuoto!");
                        }
                        reader.Dispose();
                    }
                    else
                    {
                        lgInfoStartup("loadMemConf: non trovata opzione PARAM_CONF in file INI");
                    }
                }

                // continuo setup...
                setupMemMap();
                setupOptMemPar();
                setupFileDecod();
                setupSpecialParams();

                lgInfo("loadMemConf.06");
                // inizializzo LUT decodifica ALLARMI
                if (IOBConfFull.Alarms != null)
                {
                    alarmMaps = IOBConfFull.Alarms.AlarmMaps;
                    setupAlarmMap();
                    // leggo il tipo gestione allarmi..
                    string sAlarmType = getOptJsonKVP("alarmType");
                    if (!string.IsNullOrEmpty(sAlarmType))
                    {
                        alarmType = (AlarmBlockType)Enum.Parse(typeof(AlarmBlockType), sAlarmType);
                    }
                    string sAlarmLMin = getOptJsonKVP("alarmLevelMin");
                    if (!string.IsNullOrEmpty(sAlarmLMin))
                    {
                        alarmLevelMin = (AlarmLevel)Enum.Parse(typeof(AlarmLevel), sAlarmLMin);
                    }
                    lgInfo("loadMemConf | Fine setup allarmi");
                }
                vetoReloadConf = DateTime.Now.AddMinutes(15);
                // loggo
                lgInfo($"loadMemConf.07, veto reload until {vetoReloadConf}");
                lgInfoStartup($"DONE loadMemConf, veto reload until {vetoReloadConf}");
            }
        }

        /// <summary>
        /// Recupera elenco PODL assegnabili
        /// </summary>
        /// <returns></returns>
        protected List<PODLModel> MachineNextPodl()
        {
            List<PODLModel> reqPOdlList = new List<PODLModel>();
            // per prima cosa recupero elenco PODL da gestire....
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    var rawListPODL = await callUrl(urlGetNextPODL, false);
                    if (!string.IsNullOrEmpty(rawListPODL))
                    {
                        reqPOdlList = JsonConvert.DeserializeObject<List<PODLModel>>(rawListPODL) ?? new List<PODLModel>();
                    }
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lg.Error($"Errore: chiamata MachineNextPodl: {ex.Message}");
            }
            return reqPOdlList;
        }

        /// <summary>
        /// Indica il periodo di sampling della memoria (se presente, altrimenti 0)
        /// </summary>
        /// <param name="memName"></param>
        /// <returns></returns>
        protected int memPeriod(string memName)
        {
            int period = 0;
            if (memMap != null)
            {
                if (memMap.mMapRead != null)
                {
                    //cerco in primis modalità NUM altrimenti standard...
                    if (memMap.mMapRead.ContainsKey(memName))
                    {
                        period = memMap.mMapRead[memName].period;
                    }
                }
            }
            return period;
        }

        /// <summary>
        /// Metodo da overridare x scrivere DAVVERO i parametri sul PLC
        /// NB: deve resettare reqValue
        /// </summary>
        /// <param name="updatedPar"></param>
        protected virtual void plcWriteParams(ref List<objItem> updatedPar)
        {
            // non faccio nulla di base...
        }

        /// <summary>
        /// Esegue eventuali step a valle dell'auto ODL (in caso di esito positivo autoODL)
        /// Effettuare override x casi specifici (es SiemensRama x reset contapezzi)
        /// </summary>
        protected virtual void processAutoOdlExtraStep()
        {
            // non fa nulla di default...
        }

        /// <summary>
        /// Effettua sync dati da e verso impianto
        /// </summary>
        protected virtual void ProcessDataSync()
        {
        }

        /// <summary>
        /// Ponte esecuzione processi asunc
        /// </summary>
        /// <returns></returns>
        protected virtual bool ProcessFileImport()
        {
            bool fatto = false;
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    fatto = await ProcessFileImportAsync();
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in ProcessFileImportAsync{Environment.NewLine}{ex.Message}");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua eventuale file import, archiviando file importati
        /// - es gestione file excel di Giacovelli
        /// - es gestione ritorno ricette FIMAT
        /// </summary>
        /// <returns></returns>
        protected virtual async Task<bool> ProcessFileImportAsync()
        {
            bool answ = false;

            /* ------------------------------------------
             * esecuzione cablata, si potrebbe costruire una cosa più flessibile tramite conf:
             * Oggetto MultiStepFileImport
             *  {
             *      StepName:   Step01
             *      ProcType:   Excel2Json
             *      FolderIn:   xxx
             *      FolderOut:  xxx
             *      FolderErr:  xxx
             *  },{
             *      StepName:   Step02
             *      ProcType:   Json2RegGiac
             *      FolderIn:   xxx
             *      FolderOut:  xxx
             *      FolderErr:  xxx
             *  }
             * ------------------------------------------*/
            if (!string.IsNullOrEmpty(fileImportFolder))
            {
                // verifico esistenza folders...
                if (Directory.Exists(fileImportFolder))
                {
                    string errore = "";
                    // verifico archivio
                    string dirArchive = Path.Combine(fileImportFolder, "archive");
                    baseUtils.checkDir(dirArchive);
                    string dirConvert = Path.Combine(fileImportFolder, "converted");
                    baseUtils.checkDir(dirConvert);
                    Dictionary<string, TimeSpan> statsColl = new Dictionary<string, TimeSpan>();
                    // cerco files da convertire
                    var listFiles2Conv = Directory.GetFiles(fileImportFolder, fileImportType);
                    // se li trovo --> chiamo import!
                    if (listFiles2Conv != null && listFiles2Conv.Length > 0)
                    {
                        // leggo file conf di riferimento
                        FileProcMan fpm = new FileProcMan(dirArchive, dirConvert, exclToolDirPath, "ExcImport.exe", "RefExcConv.json", "DB Loco");
                        int idxODL = 0;
                        foreach (var fileItem in listFiles2Conv)
                        {
                            // calcolo ODL x il file...
                            string fileName = Path.GetFileNameWithoutExtension(fileItem);
                            DateTime dataDoc = DateTime.Today;
                            DateTime.TryParse(fileName, out dataDoc);
                            // cerco lotto x giornata...
                            string sIdxODL = utils.callUrl(urlGetOdlAtDate(dataDoc));
                            int.TryParse(sIdxODL, out idxODL);
                            // chiamo conversione
                            TimeSpan timeElaps = fpm.doProcess(fileItem, idxODL);
                            statsColl.Add($"ExcImport conversion executed for {fileItem}", timeElaps);
                            lgTrace(($"ExcImport conversion executed for {fileItem} | {timeElaps.Milliseconds}ms"));
                        }
                    }

                    // cerco i file json x invio
                    var listFiles2Upload = Directory.GetFiles(dirConvert, "*.json");
                    // se li trovo --> chiamo import!
                    if (listFiles2Upload != null && listFiles2Upload.Length > 0)
                    {
                        foreach (var fileItem in listFiles2Upload)
                        {
                            // leggo il file json
                            Dictionary<int, BatchRec> list2Send = new Dictionary<int, BatchRec>();
                            string rawData = File.ReadAllText(fileItem);
                            if (!string.IsNullOrEmpty(rawData))
                            {
                                var convData = JsonConvert.DeserializeObject<Dictionary<int, BatchRec>>(rawData);
                                if (convData != null)
                                {
                                    list2Send = convData;
                                    // ora accodo contenuto file json...
                                    accodaRawData(rawTransfType.RegGiacenze, list2Send);
                                }
                            }

                            // processo invio: provo forzare send...
                            answ = await SvuotaCodaRawTransfAsync(true);
                            if (!answ)
                            {
                                errore = $"Errore in fase di invio dati SvuotaCodaRawTransfAsync x {fileItem}";
                            }
                            // archivio il file
                            else
                            {
                                // sposto file...
                                File.Move(fileItem, $"{dirArchive}/{Path.GetFileName(fileItem)}");

                                // fixme todo !!! eliminare json?!?!?
                            }

                            // log eventuali errori
                            if (!answ)
                            {
                                string dirError = $"{fileImportFolder}/errors";
                                baseUtils.checkDir(dirError);
                                // sposto file...
                                File.Move(fileItem, $"{dirError}/{Path.GetFileName(fileItem)}");
                                // segno errore!
                                string fileError = $"{fileImportFolder}/errors.log";
                                File.AppendAllText(fileError, errore);
                            }
                        }
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Processa le richieste di scrittura memoria
        /// </summary>
        /// <returns></returns>
        protected string processMemWriteRequests()
        {
            string answ = "";
            // li salvo nei parametri in memoria locale (ogni adapter DOVREBBE salvare POI sul VERO PLC)
            List<objItem> writeList = new List<objItem>();
            List<objItem> updatedPar = new List<objItem>();
            string currOut = "";
            // recupero elenco delle cose da fare
            string resp = getParams2write();
            if (!string.IsNullOrEmpty(resp))
            {
                try
                {
                    writeList = JsonConvert.DeserializeObject<List<objItem>>(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;
                                currOut = $" | Parameter {item.uid} | {item.value} --> {item.reqValue}";
                                // sistemo valori
                                item.value = item.reqValue;
                                // forzo variabile writeable
                                item.writable = true;
                                lgInfo($"Richiesta update parametro {currOut}");
                                // salvo in lista da ritrasmettere
                                updatedPar.Add(item);
                            }
                            else
                            {
                                currOut = $" | Error: parameter {item.uid} not found";
                                lgError($"Errore {currOut}");
                            }
                            // accodo in stringa taskVal...
                            answ += currOut;
                        }
                        // richiamo scrittura parametri su PLC
                        plcWriteParams(ref updatedPar);
                        // invio su cloud parametri!
                        string rawData = JsonConvert.SerializeObject(updatedPar);
                        utils.callUrl($"{urlUpdateWriteParams}", rawData);
                        lgInfo($"Notifica a server scrittura {updatedPar.Count} parametri");
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in processMemWriteRequests:{Environment.NewLine}{exc}");
                }
            }
            else
            {
                lgError("Non è stata ricevuta risposta x task da eseguire");
            }
            return answ;
        }

        /// <summary>
        /// Wrapper sync process other info
        /// </summary>
        /// <param name="keyReq"></param>
        /// <param name="valReq"></param>
        /// <returns></returns>
        protected virtual bool ProcessOtherInfo(string keyReq, string valReq)
        {
            bool fatto = false;
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    fatto = await ProcessOtherInfoAsync(keyReq, valReq);
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in ProcessOtherInfo | keyReq: {keyReq} | valReq: {valReq}{Environment.NewLine}{ex.Message}");
            }
            return fatto;
        }

        /// <summary>
        /// Cerca di recuperare i file generati dall'impianto in merito al processing degli ordini
        /// </summary>
        /// <returns></returns>
        /// <summary>
        /// Processing di dati "OtherInfo" da implementare caso x caso (qui riportato caso FIMAT ricette...)
        /// </summary>
        /// <param name="keyReq"></param>
        /// <param name="valReq"></param>
        protected virtual async Task<bool> ProcessOtherInfoAsync(string keyReq, string valReq)
        {
            bool answ = false;
            // test file import da conf... se hasRecipe=true --> modalità Tenditalia/FIMAT...
            if (hasRecipe)
            {
                string archBasePath = Path.Combine(pathList["fullPath-locBase"], pathList["fullPath-03-Recv"]);
                string reportBasePath = pathList["fullPath-outReport"];
                baseUtils.checkDir(reportBasePath);
                // verifico x cosa è stato richiesto attività (che esista la folder...)
                string folderPath = Path.Combine(archBasePath, keyReq);
                if (Directory.Exists(folderPath))
                {
                    // calcolo il nome zip di destinazione finale
                    int anno = DateTime.Today.Year;
                    int.TryParse(keyReq.Substring(0, 4), out anno);
                    string zipDir = Path.Combine(archBasePath, $"{anno}");
                    baseUtils.checkDir(zipDir);
                    string zipName = Path.Combine(zipDir, $"{keyReq}.zip");
                    // gestione report OUT
                    string reportPath = Path.Combine(reportBasePath, $"{keyReq}");
                    // verifico il tipo di attività e procedo
                    switch (valReq)
                    {
                        case "Arch":
                            // solo archiviazione ZIP della folder...
                            answ = doZipArchiveFolder(folderPath, zipName);
                            // se sent --> elimino record da REDIS (locale e remoto)
                            if (answ)
                            {
                                await RecipeRemoveWeekStatus(keyReq);
                            }
                            break;

                        case "SendArch":
                            // preparazione file di resoconto contumi
                            answ = RecipeDoConsumeReport(folderPath, reportPath);
                            if (answ)
                            {
                                // infine archiviazione ZIP della folder...
                                bool okArchive = doZipArchiveFolder(folderPath, zipName);
                                // se sent --> elimino record da REDIS (locale e remoto)
                                if (okArchive)
                                {
                                    await RecipeRemoveWeekStatus(keyReq);
                                }
                            }
                            break;

                        default:
                            break;
                    }
                }
            }
            return answ;
        }

        protected virtual async Task<bool> ProcessRecipeFileRetAsync()
        {
            bool answ = false;
            // test file import da conf... se hasRecipe=true --> modalità Tenditalia/FIMAT...
            if (hasRecipe)
            {
                if (pathList.Count == 0)
                {
                    lg.Debug("Attenzione: pathList vuoto!!!");
                }
                else
                {
                    // recupera i NUOVI file e li sposta in folder locale temp
                    string remoPath = Path.Combine(pathList["fullPath-locBase"], pathList["fullPath-05-remExe"]);
                    string archBasePath = Path.Combine(pathList["fullPath-locBase"], pathList["fullPath-03-Recv"]);
                    string tempPath = Path.Combine(archBasePath, "TEMP");
                    baseUtils.checkDir(tempPath);
                    bool okRetrieve = RecipeTaskDoneRetrieve(remoPath, tempPath);

                    // ora cerca nella folder locale e processa i files...
                    bool okCheck = await RecipeDoCheckFileProc(tempPath, archBasePath);

                    // verifica se sia da creare/inviare il record settimanale di consumo (dopo la
                    // mezzanotte del lunedì inizio settimana)
                    bool okSent = RecipeDoProcCons();
                }
            }
            return answ;
        }

        /// <summary>
        /// Sync archivio ricette (Macchina ‹--› MES)
        /// </summary>
        /// <returns></returns>
        protected virtual bool processRecipeSyncArch()
        {
            bool answ = false;
            // test file import da conf... se hasRecipe=true --> modalità Tenditalia/FIMAT...
            if (hasRecipe)
            {
                DateTime adesso = DateTime.Now;
                // verifico veto sync ricette (x non ripetere troppo spesso)
                if (adesso > dtVetoCheckSyncRecipe)
                {
                    // effettua sync eventuali NUOVI file ricette
                    bool okSync = RecipeDoSyncRecipe();
                    // imposto veto a 1h...
                    dtVetoCheckSyncRecipe = adesso.AddHours(1);
                }
            }
            return answ;
        }

        /// <summary>
        /// Effettua task òettura a bassa velocità
        /// </summary>
        /// <returns></returns>
        protected virtual bool processSlowDataRead()
        {
            bool answ = false;
            return answ;
        }

        protected void raiseRefresh(newDisplayData currDispData)
        {
            if (currDispData != null)
            {
                if (currDispData.hasData)
                {
                    // segnalo refresh!
                    if (eh_refreshed != null)
                    {
                        eh_refreshed(this, new iobRefreshedEventArgs(currDispData));
                    }
                }
            }
        }

        /// <summary>
        /// Registra i file in directory suddivise x anno/settimana e restitusice elenco
        /// </summary>
        /// <param name="recipeFile">Percorso file XML</param>
        /// <param name="archBasePath">Percorso base archivi</param>
        /// <returns></returns>
        protected List<string> RecipeDoArchiveWeek(string recipeFile, string archBasePath)
        {
            List<string> weeksProc = new List<string>();
            // init var
            int week = 0;
            string archPath = "";
            Calendar cal = new CultureInfo("it-IT").Calendar;
            // leggo contenuto XML
            string rawData = File.ReadAllText(recipeFile);
            // deserializza file XML x recuperare righe consumo
            XmlSerializer serializer = new XmlSerializer(typeof(ARecipe));
            using (StringReader reader = new StringReader(rawData))
            {
                try
                {
                    var currRecipe = (ARecipe)serializer.Deserialize(reader);
                    if (currRecipe != null)
                    {
                        // recupero data-ora ricetta da campo string
                        DateTime recExeDt = DateTime.Today;
                        DateTime.TryParse(currRecipe.DesRecipe.DesData.Date, out recExeDt);
                        // calcolo week da data-ora file...
                        week = cal.GetWeekOfYear(recExeDt, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
                        // verifico se ci sia la settimana indicata in elenco...
                        string currWeek = $"{recExeDt:yyyy}-{week:00}";
                        if (!weeksProc.Contains(currWeek))
                        {
                            weeksProc.Add(currWeek);
                        }
                        // sposto file x in area week
                        string rName = Path.GetFileName(recipeFile);
                        archPath = Path.Combine(archBasePath, $"{recExeDt:yyyy}-{week:00}");
                        baseUtils.checkDir(archPath);
                        string destPath = Path.Combine(archPath, rName);
                        // sposto files
                        File.Move(recipeFile, destPath);
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in RecipeDoArchiveWeek{Environment.NewLine}{exc}");
                }
            }
            return weeksProc;
        }

        /// <summary>
        /// Verifica i file recuperati
        /// - PODL da inviare a MAPO
        /// - accumulo consumo materiali
        /// </summary>
        /// <param name="localPath"></param>
        /// <param name="archBasePath"></param>
        /// <returns></returns>
        protected virtual async Task<bool> RecipeDoCheckFileProc(string localPath, string archBasePath)
        {
            bool answ = false;
            List<string> weekProcNew = new List<string>();

            // recupero elenco files...
            var fileList = Directory.GetFiles(localPath);
            if (fileList != null && fileList.Count() > 0)
            {
                foreach (var recipeFile in fileList)
                {
                    // processo eventuale apertura/chiusura PODL
                    bool okPOdl = RecipeDoProcessPODL(recipeFile);

                    // processa i file archiviando x settimana
                    var procWeeks = RecipeDoArchiveWeek(recipeFile, archBasePath);

                    // aggiungo all'elenco settimane SE mancassero le sett inserite
                    foreach (var item in procWeeks)
                    {
                        if (!weekProcNew.Contains(item))
                        {
                            weekProcNew.Add(item);
                        }
                    }
                }
                if (weekProcNew.Count > 0)
                {
                    // predispongo azioni di base ammesse
                    Dictionary<string, string> redHashWeek = new Dictionary<string, string>();
                    Dictionary<string, string> stdActList = new Dictionary<string, string>();
                    stdActList.Add("Arch", "Archivia");
                    stdActList.Add("SendArch", "Invia + Archivia");
                    List<PeriodInfo> weekHashUpdate = new List<PeriodInfo>();
                    // aggiorna in REDIS (hashList) status delle settimane coinvolte
                    foreach (var cWeek in weekProcNew)
                    {
                        string weekPath = Path.Combine(archBasePath, $"{cWeek}");
                        var weekFileList = Directory.GetFiles(weekPath, "*.xml");
                        PeriodInfo cPerInfo = new PeriodInfo()
                        {
                            NumFilesReceived = weekFileList.Count(),
                            CurrStatus = "Ricevute",
                            ActionList = stdActList
                        };
                        string rawWeek = JsonConvert.SerializeObject(cPerInfo);
                        redHashWeek.Add(cWeek, rawWeek);
                    }
                    // salvo in redis...
                    string fullKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:WeekStats");
                    var okHashDict = redisMan.redSaveHashDict(fullKey, redHashWeek);

                    // invio ANCHE in MP-IO l'update delle info...
                    string remUrl = urlSetHashDict;
                    string dictPayload = JsonConvert.SerializeObject(redHashWeek);
                    //await callUrlWithPayloadAsync(remUrl, dictPayload, false);
                    await callUrlWithPayloadAsync(remUrl, dictPayload, true);
                }
            }

            return answ;
        }

        /// <summary>
        /// Verifica se si debba registrare/inviare il report periodico di consumo indicato
        /// </summary>
        /// <returns></returns>
        protected bool RecipeDoProcCons()
        {
            bool answ = false;
            // calcolo quale sia la settimana corrente...
            int week = 0;
            DateTime adesso = DateTime.Now;
            Calendar cal = new CultureInfo("it-IT").Calendar;
            // calcolo week da data-ora file...
            week = cal.GetWeekOfYear(adesso, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            // verifico se ci sia la settimana indicata in elenco...
            string currWeek = $"{adesso:yyyy}-{week:00}";
            // recupero elenco delle settimane da processare da redis/WeekStats
            string fullKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:WeekStats");
            Dictionary<string, string> currStats = redisMan.redGetHashDict(fullKey);
            if (currStats != null && currStats.Count > 0)
            {
                // definisco il DICT delle settimane da processare (= settimane passate, NON la corrente...)
                foreach (var weekData in currStats)
                {
                    if (weekData.Key != currWeek)
                    {
                        // ...x ogni settimana PASSATA effettua SendArch... --> lancia un task!
                        ProcessOtherInfo(weekData.Key, "SendArch");
                    }
                }
                answ = true;
            }
            return answ;
        }

        /// <summary>
        /// Acquisisce eventuali nuove ricette
        /// </summary>
        /// <returns></returns>
        protected bool RecipeDoSyncRecipe()
        {
            bool answ = false;
            DateTime asdesso = DateTime.Now;
            try
            {
                string recLocalArchPath = Path.Combine(pathList["fullPath-locBase"], pathList["fullPath-00-Arch"]);
                string recRemArchPath = pathList["fullPath-06-remRec"];
                // leggo da redis (se disponibile) elenco hast file/MD5....
                string fullKey = redisMan.redHash($"IOB:Remote:{IOBConfFull.General.FilenameIOB}:FileCheck");
                Dictionary<string, string> dictRecMd5 = redisMan.redGetHashDict(fullKey);
                // leggo la directory remota...
                var remoteList = Directory.GetFiles(recRemArchPath);
                string fileName = "";
                string newFilePath = "";
                bool doAcquire = false;
                int recNum = 0;
                int newNum = 0;
                string currMd5;
                foreach (var remoteFile in remoteList)
                {
                    recNum = 0;
                    doAcquire = false;
                    // verifico MD5...
                    currMd5 = baseUtils.GetFileMd5(remoteFile);
                    fileName = Path.GetFileName(remoteFile);
                    // verifo se c'è il file...
                    if (!dictRecMd5.ContainsKey(fileName))
                    {
                        dictRecMd5.Add(fileName, currMd5);
                        doAcquire = true;
                    }
                    else
                    {
                        //// verifico SE sia modificato da meno di 30 gg...
                        //DateTime lastMod = File.GetLastWriteTime(remoteFile);
                        if (dictRecMd5[fileName] != currMd5)
                        {
                            dictRecMd5[fileName] = currMd5;
                            doAcquire = true;
                        }
                    }

                    // se devo aggiungere processo --> fix numRicetta + 10'000
                    if (doAcquire)
                    {
                        answ = true;
                        // leggo contenuto...
                        string recContent = File.ReadAllText(remoteFile);
                        // calcolo nuovo nome
                        var namePart = fileName.Split('.');
                        int.TryParse(namePart[0], out recNum);
                        newNum = recNum + 10000;
                        // modifico ricetta
                        recContent = recContent.Replace($"<RecName>{namePart[0]}</RecName>", $"<RecName>{newNum}</RecName>");
                        // salvo ricetta modificata in archivio
                        newFilePath = Path.Combine(recLocalArchPath, $"{newNum}.xml");
                        // elimino eventuale file precedente...
                        if (File.Exists(newFilePath))
                        {
                            File.Delete(newFilePath);
                        }
                        File.WriteAllText(newFilePath, recContent);
                    }
                }
                // se ho importato qualcosa...
                if (answ)
                {
                    // salvo hash dei files...
                    redisMan.redSaveHashDict(fullKey, dictRecMd5);
                }
            }
            catch (Exception exc)
            {
                lgError($"Eccezione durante RecipeDoSyncRecipe{Environment.NewLine}{exc}");
            }
            return answ;
        }

        /// <summary>
        /// restitusice i record di consumo dei materiali associati al file:
        /// </summary>
        /// <param name="recipeFile">Percorso file XML</param>
        /// <returns></returns>
        protected List<ConsRec> RecipeGetCons(string recipeFile)
        {
            List<ConsRec> listConsumi = new List<ConsRec>();
            // leggo contenuto XML
            string rawData = File.ReadAllText(recipeFile);
            // deserializza file XML x recuperare righe consumo
            XmlSerializer serializer = new XmlSerializer(typeof(ARecipe));
            using (StringReader reader = new StringReader(rawData))
            {
                try
                {
                    var currRecipe = (ARecipe)serializer.Deserialize(reader);
                    if (currRecipe != null)
                    {
                        if (currRecipe.ColRecipe != null && currRecipe.ColRecipe.Count > 0)
                        {
                            List<ColData> RigheConsumi = new List<ColData>();
                            foreach (var rigaComp in currRecipe.ColRecipe)
                            {
                                ColData datiComp = rigaComp.ColData;
                                RigheConsumi.Add(datiComp);
                            }
                            // recupero data-ora ricetta da campo string
                            DateTime recExeDt = DateTime.Today;
                            DateTime.TryParse(currRecipe.DesRecipe.DesData.Date, out recExeDt);
                            // converto le righe di consumo ColData in ConsRec
                            listConsumi = RigheConsumi.Select(x => new ConsRec()
                            {
                                FileRef = recipeFile,
                                DtRif = recExeDt,
                                ColourCode = x.ColourCode,
                                Description = x.Description,
                                WeightGrTot = x.Weightgrtotal
                            }).ToList();
                        }
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in RecipeGetCons{Environment.NewLine}{exc}");
                }
            }
            return listConsumi;
        }

        /// <summary>
        /// Prepara la ricetta indicata partendo dai dati della ricetta template + dati ODL di riferimento
        /// </summary>
        /// <param name="recFilePath">Path del file template della ricetta</param>
        /// <param name="podlReq">Record del PODL da inviare</param>
        /// <returns>Path della ricetta da inviare</returns>
        protected virtual bool RecipePrepare(string tempPath, string recFilePath, PODLModel podlReq)
        {
            bool fatto = false;
            // leggo il file template ricetta
            var rawData = File.ReadAllText(recFilePath);
            string fixContent = rawData;
            // eseguo sostituzioni
            foreach (var rule in RecipeReplRules)
            {
                string replVal = rule.Value;
                // calcolo la sostituzione per i valori SPECIFICI...
                if (replVal.Contains("{{PODL}}"))
                {
                    replVal = replVal.Replace("{{PODL}}", $"PODL{podlReq.IdxPromessa:00000000}");
                }
                if (replVal.Contains("{{Qty}}"))
                {
                    replVal = replVal.Replace("{{Qty}}", $"{podlReq.NumPezzi}");
                }
                if (replVal.Contains("{{Note}}"))
                {
                    replVal = replVal.Replace("{{Note}}", $"{podlReq.Note}");
                }
                fixContent = fixContent.Replace(rule.Key, replVal);
            }
            // scrivo file (secondo quantità...)!
            string destPath = Path.Combine(tempPath, $"PODL{podlReq.IdxPromessa:00000000}.xml");
            if (podlReq.NumPezzi <= maxQtyPerFile)
            {
                File.WriteAllText(destPath, fixContent);
                fatto = true;
            }
            else
            {
                int numReq = (int)Math.Ceiling((double)podlReq.NumPezzi / maxQtyPerFile);
                // splitto e scrivo...
                for (int i = 1; i <= numReq; i++)
                {
                    destPath = Path.Combine(tempPath, $"PODL{podlReq.IdxPromessa:00000000}-{i}.xml");
                    File.WriteAllText(destPath, fixContent);
                }
                fatto = true;
            }
            return fatto;
        }

        /// <summary>
        /// Prepara le ricette dato fullPath temp scaricando elenco PODL
        /// </summary>
        /// <param name="tempPath">
        /// Percorso temp di appoggio x preparare ricette (compreso nome macchina)
        /// </param>
        /// <param name="useLocal">
        /// Indica se usare le copie locali delle ricette oppure richiedere da remoto (REST get)
        /// </param>
        /// <returns></returns>
        protected virtual bool RecipeReqWriteLocal(string tempPath, bool useLocal)
        {
            bool fatto = false;
            lgTrace($"RecipeReqWriteLocal start | tempPath {tempPath} | useLocal {useLocal}");

            List<PODLModel> reqPOdlList = new List<PODLModel>();
            // per prima cosa recupero elenco PODL da gestire....
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    var rawListPODL = await callUrl(urlGetNextPODL, false);
                    if (!string.IsNullOrEmpty(rawListPODL))
                    {
                        try
                        {
                            reqPOdlList = JsonConvert.DeserializeObject<List<PODLModel>>(rawListPODL);
                        }
                        catch (Exception exc)
                        {
                            lg.Error($"Errore: chiamata elenco PODL ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lg.Error($"Errore: chiamata elenco PODL ({urlGetNextPODL}) ha restituito valore vuoto");
                    }
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in RecipeReqWriteLocal{Environment.NewLine}{ex.Message}");
            }
            // proseguo se ne ho trovati...
            if (reqPOdlList.Count > 0)
            {
                // in questo elenco devo considerare solo PODL ATTIVI...
                List<PODLModel> actPOdlList = reqPOdlList.Where(x => x.Attivabile).ToList();
                // da questo elenco,se è rimasto qualcosa, ciclo x ricette da inviare
                foreach (var actPodlReq in actPOdlList)
                {
                    // devo escludere i PODL già gestiti
                    if (!POdlSentList.ContainsKey(actPodlReq.IdxPromessa))
                    {
                        string recFilePath = "";
                        try
                        {
                            // calcolo fullPath ricetta...
                            recFilePath = Path.Combine(pathList["fullPath-locBase"], pathList["fullPath-00-Arch"], actPodlReq.Recipe);
                            // preparo file ricetta x PODL
                            bool fileOk = RecipePrepare(tempPath, recFilePath, actPodlReq);
                            if (fileOk)
                            {
                                // aggiungo PODL ad elenco dei processati
                                POdlSentList.Add(actPodlReq.IdxPromessa, actPodlReq);
                                // indico okReport (almeno per 1 è ok...)
                                fatto = true;
                            }
                        }
                        catch (Exception exc)
                        {
                            lgError($"recipeReqWriteLocal | Errore durante ciclo verifica ricetta | idxPODL {actPodlReq.IdxPromessa} | recFilePath {recFilePath}{Environment.NewLine}{exc}");
                        }
                    }
                }
                // salvo su file elenco PODL gestiti/inviati
                POdlSentFileArch = POdlSentList;
                lgInfo($"RecipeReqWriteLocal | reqPOdlList: {reqPOdlList.Count} | actPOdlList: {actPOdlList.Count} | preparate: {POdlSentList.Count}");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua invio delle ricette alla macchina
        /// </summary>
        /// <param name="localPath">Area dove si trovano i fiel da trasmettere</param>
        /// <param name="localArch">Area archivio</param>
        /// <param name="remotePath">Area remota dove inviare files</param>
        /// <returns></returns>
        protected bool RecipeSend(string localPath, string localArch, string remotePath)
        {
            bool fatto = false;
            lgTrace($"RecipeSend start | localPath {localPath} | localArch {localArch} | remotePath {remotePath}");
            // recupero elenco files...
            var fileList = Directory.GetFiles(localPath);
            string archName = "";
            string destName = "";
            if (fileList != null && fileList.Count() > 0)
            {
                int nMove = 0;
                foreach (var file in fileList)
                {
                    string fileName = Path.GetFileName(file);
                    archName = Path.Combine(localArch, fileName);
                    destName = Path.Combine(remotePath, fileName);
                    lgInfo($"RecipeSend | fileName: {fileName} | archName: {archName} | destName: {destName} ");
                    File.Copy(file, destName, true);
                    File.Move(file, archName);
                    nMove++;
                }
                fatto = true;
                lgInfo($"RecipeSend | trovate  {fileList.Count()} file | {nMove} trasferiti");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua recupero in locale dei task eseguiti dalla macchina
        /// </summary>
        /// <param name="remotePath">Area remota da dove recuperare files</param>
        /// <param name="localPath">Area dove parcheggiare temporaneamente i fiels x processing</param>
        /// <returns></returns>
        protected virtual bool RecipeTaskDoneRetrieve(string remotePath, string localPath)
        {
            bool fatto = false;
            // recupero elenco files...
            var fileList = Directory.GetFiles(remotePath);
            string destName = "";
            if (fileList != null && fileList.Count() > 0)
            {
                foreach (var file in fileList)
                {
                    string fileName = Path.GetFileName(file);
                    destName = Path.Combine(localPath, fileName);
                    lgInfo($"Recupero Task Eseguiti | fileName: {fileName} | destName: {destName} ");
                    File.Move(file, destName);
                }
                fatto = true;
            }
            return fatto;
        }

        /// <summary>
        /// Cancella dal server i task eseguiti
        /// </summary>
        /// <param name="taskName"></param>
        /// <param name="esitoTask"></param>
        /// <param name="codTav"></param>
        /// <returns></returns>
        protected async Task<string> remTask2exe(string taskName, string esitoTask, string codTav)
        {
            string answ = "";
            bool srvAlive = await CheckServerAliveAsync();
            if (srvAlive)
            {
                string url2call = $"{urlRemTask2Exe}{taskName}";
                if (!string.IsNullOrEmpty(codTav))
                {
                    url2call = $"{urlRemTask2ExeTav(codTav)}{taskName}";
                }

                await utils.callUrlAsync(url2call);
            }
            return answ;
        }

        /// <summary>
        /// Salva nel dizionario il num di valori letti e filtrati/non inviati
        /// </summary>
        /// <param name="codFlux"></param>
        protected void SaveFiltFluxLog(string codFlux)
        {
            if (DictFiltFLog.ContainsKey(codFlux))
            {
                DictFiltFLog[codFlux]++;
            }
            else
            {
                DictFiltFLog.Add(codFlux, 1);
                _logger.Info($"FluxLog | veto | {codFlux}");
            }
        }

        /// <summary>
        /// Invio reset allarmi all'avvio per sicurezza...
        /// </summary>
        protected void SendAlarmReset()
        {
            if (alarmMaps != null)
            {
                // leggo a ciclo le aree degli allarmi CONFIGURATI, se ne trovo processo
                foreach (var item in alarmMaps)
                {
                    // banchi in array Int16 --> scompongo
                    for (int i = 0; i < item.size / 2; i++)
                    {
                        // INVIO CON LAST = 1 E NEW = 0 PER FORZARE INVIO...
                        sendAlarmVariations(item.memAddr, i, 1, 0, item.messages);
                    }
                }
            }
        }

        /// <summary>
        /// Processa chiusura ODL
        /// </summary>
        /// <param name="idxOdl">Idx dell'ODL da chiudere</param>
        /// <param name="dtRif">DataOra di riferimento evento stop</param>
        protected async Task<bool> SendCloseOdl(int idxOdl, DateTime dtRif)
        {
            bool answ = false;
            DateTime dtCurr = DateTime.Now;
            string resp = await callUrl($"{urlODLClose}{idxOdl}&dtEve={dtRif}&dtCurr={dtCurr}", false);
            answ = resp == "OK";
            return answ;
        }

        /// <summary>
        /// Processa chiusura PODL
        /// </summary>
        /// <param name="idxPOdl">Idx del PODL da chiudere</param>
        /// <param name="dtRif">DataOra di riferimento evento stop</param>
        protected bool SendClosePOdl(int idxPOdl, DateTime dtRif)
        {
            bool answ = false;
            DateTime dtCurr = DateTime.Now;
            string fullUrl = $"{urlPODLClose}{idxPOdl}&dtEve={dtRif:yyyyMMddHHmmssfff}&dtCurr={dtCurr:yyyyMMddHHmmssfff}";
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    // invio chiamata URL x chiusura ODL su macchina
                    string callResp = await callUrl(fullUrl, false);
                    answ = callResp == "OK";
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in SendClosePOdl | {Environment.NewLine}{ex.Message}");
            }
            return answ;
        }

        /// <summary>
        /// Verifica se l'invio di un dato flux log sia abilitato o vietato secondo configurazione
        /// - verifica rpesenza di un veto preesistente non scaduto
        /// - se manca. impone nuovo veto dal "period" impostato in aree memMap (default: 60 sec)
        /// </summary>
        /// <param name="codFlux"></param>
        /// <returns></returns>
        protected bool sendEnabledFLog(string codFlux)
        {
            int vetoSec = vetoCodFluxDur(codFlux);
            DateTime adesso = DateTime.Now;
            bool hasVeto = vetoSendFLog.ContainsKey(codFlux);
            // se ho un veto controllo scadenza...
            if (hasVeto)
            {
                hasVeto = vetoSendFLog[codFlux] >= adesso;
                // se fosse scaduto --> imposto nuovo...
                if (!hasVeto)
                {
                    vetoSendFLog[codFlux] = adesso.AddSeconds(vetoSec);
                }
            }
            // se NON ci fosse veto o fosse scaduto --> imposto nuovo...
            else
            {
                vetoSendFLog.Add(codFlux, adesso.AddSeconds(vetoSec));
            }
            // restituisco send enabled = NON ha veto
            return !hasVeto;
        }

        /// <summary>
        /// Invia informazioni associazione IOB 2 machine
        /// </summary>
        protected void SendM2IOB()
        {
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () => await SendM2IobAsync())
                    .GetAwaiter()
                    .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in SendM2IOB: {ex.Message}");
            }
        }

        /// <summary>
        /// Invia informazioni associazione IOB 2 machine
        /// </summary>
        protected async Task SendM2IobAsync()
        {
            if (await CheckServerAliveAsync())
            {
                string sendKey = "SendM2Iob";
                bool sendEnab = CheckSendVeto(sendKey, TimeSpan.FromMinutes(120));
                // se abilitato faccio invio e salvo nuovo valore
                if (sendEnab)
                {
                    // salvo nuovo valore invio
                    LastSendSet(sendKey, DateTime.Now);
                    // procedo
                    var result = await utils.callUrlAsync(urlSetM2IOB);
                    lgInfo($"chiamata URL {urlSetM2IOB} | result: {result}");
                }
            }
        }

        /// <summary>
        /// Ponte Async invio MachineConf
        /// </summary>
        protected virtual void SendMachineConf()
        {
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () => await SendMachineConfAsync())
                    .GetAwaiter()
                    .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in SendMachineConf: {ex.Message}");
            }
        }

        /// <summary>
        /// Effettua invio al server IO dei dati di IOB + macchina:
        /// - dati dell'IOB (IdxMacchina, IobName, IobIp, Tipo, counter...) "classici"
        /// - dati di configurazione IOB "extra", ad esempio
        /// - IobType (FANUC, Siemens, ModBus...)
        /// - MachineIp
        /// - MachinePort
        /// - Abilitazione gestione FOLDER di ritorno x SPEC (es x FTP)
        /// </summary>
        protected virtual async Task SendMachineConfAsync()
        {
            // se non espressamente vietato da conf...
            if (enabSendMachineConf)
            {
                if (await CheckServerAliveAsync())
                {
                    string sendKey = "SendMachineConf";
                    bool sendEnab = CheckSendVeto(sendKey, TimeSpan.FromMinutes(120));

                    // se abilitato faccio invio e salvo nuovo valore
                    if (sendEnab)
                    {
                        // salvo nuovo valore invio
                        LastSendSet(sendKey, DateTime.Now);

                        // preparo dizionario da inviare
                        Dictionary<string, string> currDict = new Dictionary<string, string>();
                        Assembly entryAssembly = Assembly.GetEntryAssembly();
                        if (IOBConfFull != null)
                        {
                            // creo genObj dati necessari...
                            currDict.Add("IobName", IOBConfFull.General.FilenameIOB);
                            currDict.Add("IobType", $"{IOBConfFull.General.IobType}");
                            currDict.Add("MachIp", IOBConfFull.Device.Connect.IpAddr);
                            currDict.Add("MachPort", IOBConfFull.Device.Connect.Port);
                            currDict.Add("Vendor", IOBConfFull.Device.Vendor);
                            currDict.Add("Model", IOBConfFull.Device.Model);
                            currDict.Add("IobExe", $"{entryAssembly?.GetName().Name}");
                            currDict.Add("IobVersion", IOBConfFull.General.RelVers);
                            if (IOBConfFull.OptPar != null)
                            {
                                var ordPar = IOBConfFull.OptPar.OrderBy(x => x.Key).ToList();
                                foreach (var item in ordPar)
                                {
                                    currDict.Add($"OP_{item.Key}", item.Value);
                                }
                            }
                            // invio e salvo...
                            string remUrl = urlSaveMachIobConf;
                            string dictPayload = JsonConvert.SerializeObject(currDict);
                            await callUrlWithPayloadAsync(remUrl, dictPayload, true);
                            lgTrace("Invio MachineConf effettuato");
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Invia al server IO i valori dei parametri opzionali (es counters)
        /// </summary>
        /// <param name="paramName">Nome parametro</param>
        /// <param name="paramValue">Valore parametro</param>
        protected async Task sendOptVal(string paramName, string paramValue)
        {
            // salvo comunque in fluxLog...
            bool sent = accodaFLog(paramName, $"{paramName}|{paramValue}", qEncodeFLog(paramName, paramValue));
            if (sent)
            {
                // traccio valore DynData x analisi
                trackDynData(paramName, paramValue);
                bool srvAlive = await CheckServerAliveAsync();
                if (srvAlive)
                {
                    string url2call = $"{urlSetOptVal}pName={paramName}&pValue={paramValue}";
                    lgInfo("chiamata URL " + url2call);
                    await utils.callUrlAsync(url2call);
#if false
                    utils.callUrlNow(url2call);
#endif
                }
            }
        }

        /// <summary>
        /// Invia al server IO i valori dei parametri opzionali (es counters)
        /// </summary>
        /// <param name="paramName">Nome parametro</param>
        /// <param name="paramValueInt">Valore parametro INT</param>
        protected async Task sendOptVal(string paramName, int paramValueInt)
        {
            // override!
            await sendOptVal(paramName, $"{paramValueInt}");
        }

        /// <summary>
        /// Invio contapezzi alla dataora indicata
        /// </summary>
        /// <param name="numPz">Num pezzi da registrare</param>
        /// <param name="dtRif">DataOra di riferimento evento</param>
        protected bool SendPzIncrAtDate(int numPz, DateTime dtRif)
        {
            bool answ = false;
            DateTime dtCurr = DateTime.Now;
            // chiamata avvio PODL (a posteriori)
            string fullUrl = $"{urlAddPzCountAtDate}{numPz}&dtEve={dtRif:yyyyMMddHHmmssfff}&dtCurr={dtCurr:yyyyMMddHHmmssfff}";
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    // invio chiamata URL x chiusura ODL su macchina
                    string callResp = await callUrl(fullUrl, false);
                    answ = callResp == "OK";
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in SendPzIncrAtDate | {Environment.NewLine}{ex.Message}");
            }
            return answ;
        }

        /// <summary>
        /// Processa avvio PODL (ed eventuale chiusura precedente)
        /// </summary>
        /// <param name="idxPOdl">Idx del PODL da avviare</param>
        /// <param name="dtRif">DataOra di riferimento evento start</param>
        protected bool SendStartPodl(int idxPOdl, DateTime dtRif)
        {
            bool answ = false;
            DateTime dtCurr = DateTime.Now;
            // chiamata avvio PODL (a posteriori)
            string fullUrl = $"{urlOdlStartFromPOdl}{idxPOdl}&dtEve={dtRif:yyyyMMddHHmmssfff}&dtCurr={dtCurr:yyyyMMddHHmmssfff}";
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    // invio chiamata URL x chiusura ODL su macchina
                    string callResp = await callUrl(fullUrl, false);
                    answ = callResp == "OK";
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in SendStartPodl | {Environment.NewLine}{ex.Message}");
            }
            return answ;
        }

        /// <summary>
        /// Invia messaggio a logWatcher
        /// </summary>
        /// <param name="messType"></param>
        /// <param name="message"></param>
        protected void sendToTaskWatch(string messType, string message, string codTav)
        {
            string logMsg = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} | {messType} | {message}";
            if (string.IsNullOrEmpty(codTav))
            {
                logMsg += $" | {messType} | {message}";
            }
            else
            {
                logMsg += $" | {codTav} | {messType} | {message}";
            }
            parentForm.taskWatcher = logMsg;
        }

        /// <summary>
        /// Impostazioni parametri PLC
        /// </summary>
        protected virtual void setParamPlc()
        {
            loadMemConf();
            fixDefaultPar();
            if (resetAlarmOnStart)
            {
                SendAlarmReset();
            }
        }

        /// <summary>
        /// setup gestione allarmi da conf
        /// </summary>
        protected void setupAlarmMap()
        {
            // indico quanti allarmi
            foreach (var item in alarmMaps)
            {
                item.setupData();
                // se ho in redis una tab di allarmi precedenti la carico...
                string alarmHash = redisMan.redHash($"IOB:ALARM_STATUS:{IOBConfFull.General.FilenameIOB}:{item.memAddr}");
                string rawVal = redisMan.getRSV(alarmHash);
                if (!string.IsNullOrEmpty(rawVal))
                {
                    // provo a convertire
                    var lastState = JsonConvert.DeserializeObject<uint[]>(rawVal);
                    if (lastState != null && lastState.Length > 0)
                    {
                        item.loadPrev(lastState);
                    }
                }
                // loggo
                lgDebug($"Decodifica aree alarmMap: {item.description} | {item.memAddr} x {item.size} byte | {item.messages.Count} messaggi allarme");
            }
            // SE NECESSARIO ALL'AVVIO INVIO DATI VUOTI...
            if (resetAlarmOnStart)
            {
                SendAlarmReset();
            }

            // invio oggetto alarmMap al server x successiva decodifica

            // FIXME TODO FARE !!!! invio PUT del file *_alarm.json
        }

        /// <summary>
        /// Setup parametri decode file excel (opzionale)
        /// </summary>
        protected virtual void setupFileDecod()
        {
            if (memMap == null)
            {
                lgWarn($"setupFileDecod | memMap nullo | no processing");
            }
            else
            {
                // verifica se siano necessari configuraizoni speciali dalla excDecod:
                // es: lettura/invio file excel
                if (memMap.FileDecod != null && memMap.FileDecod.Count > 0)
                {
                    FileDecod = memMap.FileDecod;
                }
            }
        }

        /// <summary>
        /// setup parametri da file di conf
        /// </summary>
        protected void setupMemMap()
        {
            bool serverOk = GetPingStatus() == IPStatus.Success;
            if (memMap == null)
            {
                lgWarn($"setupMemMap | memMap nullo");
            }
            else
            {
                lgDebug($"setupMemMap | trovati {memMap.mMapRead.Count} parametri Read (TSVC)");
                lgDebug($"setupMemMap | trovati {memMap.mMapWrite.Count} parametri Write");
                if (utils.CRB("verbose"))
                {
                    string rawMemConf = JsonConvert.SerializeObject(memMap, Formatting.Indented);
                    lgDebug($"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,
                            UM = item.Value.unit,
                            // imposto a NOW per forzare accumulo dati prima di inviare troppo presto (es COMECA Pizzaferri valori 0)
                            DTStart = DateTime.Now,
                            //DTStart = DateTime.Now.AddHours(-1),
                            dataArray = new List<double>()
                        };
                        TSVC_Data.Add(item.Key, currConf);
                    }
                    // log avvio + setup lastVal...
                    foreach (var item in TSVC_Data)
                    {
                        lgTrace($"TSVC: {item.Key} | periodo: {item.Value.Period} | funz: {item.Value.Funzione}");
                        // salvo i valori PREC...
                        LastTSVC.Add(item.Key, 0);
                    }
                }
                // infine se genObj memoria valido salvo in MP-IO x sue applicazioni
                if (memMap != null)
                {
                    // invio su cloud conf memoria...
                    string rawData = JsonConvert.SerializeObject(memMap, Formatting.Indented);
                    // controllo ping al server...
                    if (serverOk)
                    {
                        string sendKey = "SendMemMap";
                        bool sendEnab = CheckSendVeto(sendKey, TimeSpan.FromMinutes(480));

                        // se abilitato faccio invio e salvo nuovo valore
                        if (sendEnab)
                        {
                            var resp = utils.callUrl($"{urlSaveMemMap}", rawData);
                        }
                    }
                    else
                    {
                        lgInfo($"Mancata risposta ping da server, saltato step invio memMap a {urlSaveMemMap}");
                    }
                    // salvo ANCHE come parametri i valori...
                    objItem currItem = new objItem();
                    List<objItem> allParam = new List<objItem>();
                    string currValore = "";
                    // valori WRITE
                    foreach (var item in memMap.mMapWrite)
                    {
                        currValore = "";
                        //se ho valori current li impiego...
                        if (currProdData.ContainsKey(item.Value.name))
                        {
                            currValore = currProdData[item.Value.name];
                            item.Value.value = currValore;
                        }
                        currItem = new objItem()
                        {
                            uid = item.Value.name,
                            name = item.Value.name,
                            description = !string.IsNullOrEmpty(item.Value.description) ? item.Value.description : item.Value.name,
                            writable = true,
                            UM = item.Value.unit,
                            valMin = item.Value.minVal,
                            valMax = item.Value.maxVal,
                            value = currValore,
                            displOrdinal = item.Value.displOrdinal
                        };
                        allParam.Add(currItem);
                    }
                    // valori READ
                    foreach (var item in memMap.mMapRead)
                    {
                        // se non ci fosse aggiungo
                        var trovato = allParam.FirstOrDefault(x => x.uid == item.Value.name);
                        if (trovato == null)
                        {
                            currValore = "";
                            // se ho valori current li impiego...
                            if (currProdData.ContainsKey(item.Value.name))
                            {
                                currValore = currProdData[item.Value.name];
                                item.Value.value = currValore;
                            }
                            currItem = new objItem()
                            {
                                uid = item.Value.name,
                                name = item.Value.name,
                                description = !string.IsNullOrEmpty(item.Value.description) ? item.Value.description : item.Value.name,
                                writable = false,
                                UM = item.Value.unit,
                                valMin = item.Value.minVal,
                                valMax = item.Value.maxVal,
                                value = currValore,
                                displOrdinal = item.Value.displOrdinal
                            };
                            allParam.Add(currItem);
                        }
                    }
                    // invio su cloud parametri SE sono connesso alla macchina... in pratica reset parametri...
                    string tipoCall = urlSaveAllParams;
                    rawData = JsonConvert.SerializeObject(allParam, Formatting.Indented);
                    if (serverOk)
                    {
                        // verifica se sia un IOB "parziale" --> salva solo update ai parametri
                        if (IOBConfFull.Device.DisabExeTask || IOBConfFull.Device.DisabStateCh)
                        {
                            // versione upsert
                            tipoCall = urlUpdateWriteParams;
                        }
                        var resp = utils.callUrl($"{tipoCall}", rawData);
                    }
                    else
                    {
                        lgInfo($"Mancata risposta ping server, non effettuata chiamata {tipoCall}");
                    }
                    lgDebug($"setupMemMap | salvata conf memoria R/W");
                }
            }
        }

        /// <summary>
        /// Setup parametri memoria opzionali
        /// es: BitCond e IntCond x ricerca valori target
        /// </summary>
        protected virtual void setupOptMemPar()
        {
            if (memMap == null)
            {
                lgWarn($"setupOptMemPar | memMap nullo");
            }
            else
            {
                // verifica se siano necessari configurazioni speciali dalla OptMemPar:
                // es: per ricerca condizioni status booleane come ModbusTCP...
                if (memMap.OptMemPar != null && memMap.OptMemPar.Count > 0)
                {
                    // cerco condizioni BIT speciali x Auto, Estop, Work.. devono essere *BitCond
                    var listPar2add = memMap.OptMemPar.Where(x => x.Key.EndsWith("BitCond")).ToList();
                    if (listPar2add != null && listPar2add.Count > 0)
                    {
                        foreach (var item in listPar2add)
                        {
                            addCheckConditionBit(item.Key, item.Value);
                        }
                    }
                    // cerco condizioni INT speciali x Auto, Estop, Work.. devono essere *IntCond
                    var listParInt2add = memMap.OptMemPar.Where(x => x.Key.EndsWith("IntCond")).ToList();
                    if (listParInt2add != null && listParInt2add.Count > 0)
                    {
                        foreach (var item in listParInt2add)
                        {
                            addCheckConditionInt(item.Key, item.Value);
                        }
                    }
                    // cerco valori *2Translate
                    var listVal2Translate = memMap.OptMemPar.Where(x => x.Key.Contains("2Translate")).ToList();
                    if (listVal2Translate != null && listVal2Translate.Count > 0)
                    {
                        // cerco solo valori BIT...
                        var listValBit = listVal2Translate.Where(x => x.Key.StartsWith("VarBit2Translate")).ToList();
                        if (listValBit != null && listValBit.Count > 0)
                        {
                            foreach (var item in listValBit)
                            {
                                addVal2TranslBit(item.Value);
                            }
                        }
                        // ...e poi valori INT...
                        var listValInt = listVal2Translate.Where(x => x.Key.StartsWith("VarInt2Translate")).ToList();
                        if (listValInt != null && listValInt.Count > 0)
                        {
                            foreach (var item in listValInt)
                            {
                                addVal2TranslInt(item.Value);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Init parametri speciali, tipicamente KVP opzionali da json
        /// </summary>
        protected virtual void setupSpecialParams()
        {
            string sCond = "";
            // per prima cosa inizializzo lista PODL inviati
            POdlSentList = POdlSentFileArch;

            // verifico se sia attiva gestione riduzione FluxLog...
            if (!string.IsNullOrEmpty(getOptJsonKVP("fluxLogReduce")))
            {
                bool.TryParse(getOptJsonKVP("fluxLogReduce"), out fluxLogReduce);
                double.TryParse(getOptJsonKVP("fluxLogRedDeadBand"), NumberStyles.Any, CultureInfo.InvariantCulture, out fluxLogRedDeadBand);
                int.TryParse(getOptJsonKVP("fluxLogResendPeriod"), out fluxLogResendPeriod);
                lgInfo($"FluxLog Redux | fluxLogReduce: {fluxLogReduce} | fluxLogRedDeadBand: {fluxLogRedDeadBand:N2} | fluxLogResendPeriod: {fluxLogResendPeriod} min");
            }

            // check veto livelli allarmi/conditions
            string rawVeto = getOptJsonKVP("condLevelVeto");
            if (!string.IsNullOrEmpty(rawVeto))
            {
                char separatore = '|';
                // accetto questi separatori '|' o '#'
                int indice = rawVeto.IndexOfAny(new char[] { '|', '#' });
                if (indice >= 0)
                {
                    separatore = rawVeto[indice];
                }
                string[] valori = rawVeto.Split(separatore);
                ListVetoCond = valori.ToList();
            }

            // check modalità ricetta attiva
            bool.TryParse(getOptJsonKVP("hasRecipe"), out hasRecipe);

            // verifico se usare ricette locali/http...
            bool.TryParse(getOptJsonKVP("useLocalRecipe"), out useLocalRecipe);

            // recupero quantitativo massimo KG x PODL
            int.TryParse(getOptJsonKVP("maxPodlQty"), out maxQtyPerFile);

            if (memMap == null)
            {
                lgWarn("setupSpecialParams: memMap nullo!");
            }
            else
            {
                // recupero le folder specifiche x IN/OUT ricette filtrando direttamente l'area di memoria...
                sCond = "fullPath";
                var dirList = memMap.OptKVP
                                        .Where(x => x.Key.StartsWith(sCond))
                                        .ToList();
                pathList = dirList.ToDictionary(x => x.Key, x => x.Value);

                // gestione replace in file ricette...
                sCond = "replace-";
                var ruleList = memMap.OptKVP
                                       .Where(x => x.Key.StartsWith(sCond))
                                       .ToList();
                RecipeReplRules = ruleList.ToDictionary(x => x.Key.Replace(sCond, ""), x => x.Value);
            }
        }

        /// <summary>
        /// Cleanup stringa x impiego tipo ident da char dubbi
        /// </summary>
        /// <param name="origData"></param>
        /// <returns></returns>
        protected string strFixId(string origData)
        {
            return origData.Replace(".", "").Replace(" ", "_");
        }

        /// <summary>
        ///  Traccia in redis un dynData x analisi frequenza e campionamento valori FluxLog ...
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        protected void trackDynData(string key, string value)
        {
            // attenzione: x ora cablato 3 gg e 1 mese le registrazioni
            DateTime oggi = DateTime.Today;
            string DayCurr = $"{oggi:yyMMdd}";
            // se cambiato giorno --> forzo scrittura senza aggiunte
            if (!LastDayCurr.Equals(DayCurr))
            {
                DoTrackDataSave();
            }
            // accumulo stat Day
            DoTrackDataCount(key, value);
            // ...e se > soglia registro
            DoTrackDataSave();
        }

        /// <summary>
        ///  Traccia in redis l'attività di scambio dati (tipicamente non in polling)
        /// </summary>
        /// <param name="byteSize">Dim pacchetto (numero byte scambiati/ricevuti)</param>
        /// <param name="val2rec">Dim minima x registrare info</param>
        protected void trackExchData(long byteSize, long val2rec = 512)
        {
            // accumulo counter byte
            currByteCount += byteSize;
            // se superato limite scrivo (default 128byte)
            if (currByteCount >= val2rec)
            {
                string rkeyTS = $"{redisMan.redIobTrackKey}:DataInTS";
                string rkeyHS = $"{redisMan.redIobTrackKey}:DataInHS";
                // chiamo direttamente metodo redis...
                string newUid_ = redisMan.TrackExchData(rkeyTS, rkeyHS, currByteCount);
                currByteCount = 0;
            }
        }

        /// <summary>
        /// Track dati ricevuti generici (serializzando)
        /// </summary>
        /// <param name="genObj"></param>
        protected void trackExchDataRaw(object genObj, long val2rec = 512)
        {
            try
            {
                long byteSize = GetObjectSize(genObj, false);
                // salvo dim caratteri ricevuti
                trackExchData(byteSize, val2rec);
            }
            catch (Exception exc)
            {
                lgError($"trackExchDataRaw | errore in fase di track Obj ricevuto{Environment.NewLine}{exc}");
            }
        }

        /// <summary>
        ///  Traccia in redis l'attività di lettura dati
        /// </summary>
        /// <param name="byteSize">Dim pacchetto (numero byte letti)</param>
        /// <param name="timeSec">Se specificato indica il tempo effettivo di lettura, se zero uso timespan giornaliero...</param>
        protected void trackReadData(int byteSize, double timeSec)
        {
            // attenzione: x ora cablato 1 mese le registrazioni
            DateTime adesso = DateTime.Now;
            double kbRead = (double)byteSize / 1024;
            var cultInv = CultureInfo.InvariantCulture;
            // per prima cosa LEGGO il valore precedente
            string rKey = $"{redisMan.redIobTrackKey}:DataIn:{adesso:yyMMdd}";
            var actData = redisMan.redGetHashDict(rKey);

            // KB letti
            double dVal = 0;
            if (actData.ContainsKey("KbRead"))
            {
                double.TryParse(actData["KbRead"], NumberStyles.Float, cultInv, out dVal);
            }
            dVal += kbRead;
            string sDouble = dVal.ToString("F3", cultInv);
            if (actData.ContainsKey("KbRead"))
            {
                actData["KbRead"] = sDouble;
            }
            else
            {
                actData.Add("KbRead", sDouble);
            }

            // TIMING: se ho un valore lo aggiungo, altrimenti calcolo da mezzanotte
            double dPTime = 0;
            if (timeSec > 0)
            {
                if (actData.ContainsKey("ProcTime"))
                {
                    double.TryParse(actData["ProcTime"], NumberStyles.Float, cultInv, out dPTime);
                }
                dPTime += timeSec;
            }
            else
            {
                dPTime = adesso.Subtract(adesso.Date).TotalSeconds;
            }
            sDouble = dPTime.ToString("F3", cultInv);
            if (actData.ContainsKey("ProcTime"))
            {
                actData["ProcTime"] = sDouble;
            }
            else
            {
                actData.Add("ProcTime", sDouble);
            }

            // salvo il mio hash aggiornato!
            redisMan.redSaveHashDict(rKey, actData);
        }

        /// <summary>
        /// Reset valori trackdata all'avvio dell'adapter IOB
        /// </summary>
        protected void trackReadDataReset()
        {
            // per prima cosa LEGGO il valore precedente
            DateTime adesso = DateTime.Now;
            string rKey = $"{redisMan.redIobTrackKey}:DataIn:{adesso:yyMMdd}";
            redisMan.redDelKey(rKey);
        }

        /// <summary>
        /// Versione sync richiesta chiusura ODL corrente
        /// </summary>
        /// <returns></returns>
        protected bool TryAskCloseCurrODL()
        {
            bool fatto = false;
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    fatto = await TryAskCloseCurrODLAsync();
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TryAskCloseCurrODL{Environment.NewLine}{ex.Message}");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per chiedere all'utente se vuole effettuare chiusura ODL
        /// corrente della macchina
        /// </summary>
        /// <returns></returns>
        protected async Task<bool> TryAskCloseCurrODLAsync()
        {
            bool fatto = false;
            string fullUrl = $"{urlODLAskClose}{currIdxODL}";
            try
            {
                // invio chiamata URL x chiusura ODL su macchina
                string callResp = await callUrl(fullUrl, false);
                fatto = (callResp != "KO") ? true : false;
            }
            catch
            { }
            lgInfo($"Eseguito TryAskCloseCurrODLAsync per {currIdxODL} | url: {fullUrl} | esito: {fatto}");
            return fatto;
        }

        /// <summary>
        /// Versione sync chiusura ODL corrente
        /// </summary>
        /// <returns></returns>
        protected bool TryCloseCurrODL()
        {
            bool fatto = false;
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    fatto = await TryCloseCurrODLAsync();
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TryCloseCurrODLAsync{Environment.NewLine}{ex.Message}");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per tentare chiusura ODL corrente della macchina
        /// </summary>
        /// <returns></returns>
        protected async Task<bool> TryCloseCurrODLAsync()
        {
            bool fatto = false;
            string fullUrl = $"{urlODLClose}{currIdxODL}";
            try
            {
                // invio chiamata URL x chiusura ODL su macchina
                string callResp = await callUrl(fullUrl, false);
                fatto = (callResp != "KO") ? true : false;
            }
            catch
            { }
            lgInfo($"Eseguito TryCloseCurrODLAsync per {currIdxODL} | url: {fullUrl} | esito: {fatto}");
            return fatto;
        }

        /// <summary>
        /// Versione sync chiusura ODL specifico
        /// </summary>
        /// <param name="IdxODL"></param>
        /// <returns></returns>
        protected bool TryCloseODL(int IdxODL)
        {
            bool fatto = false;
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    fatto = await TryCloseODLAsync(IdxODL);
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TryCloseODLAsync | IdxODL: {IdxODL}{Environment.NewLine}{ex.Message}");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per tentare chiusura ODL specifico
        /// </summary>
        /// <param name="IdxODL"></param>
        /// <returns></returns>
        protected async Task<bool> TryCloseODLAsync(int IdxODL)
        {
            bool fatto = false;
            string fullUrl = $"{urlODLClose}{IdxODL}";
            try
            {
                // invio chiamata URL x chiusura ODL su macchina
                string callResp = await callUrl(fullUrl, false);
                fatto = (callResp != "KO") ? true : false;
            }
            catch
            { }
            lgInfo($"Eseguito TryCloseODLAsync per {IdxODL} | url: {fullUrl} | esito: {fatto}");
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per tentare chiusura PODL --&gt; ODL specifico
        /// </summary>
        /// <param name="idxPODL"></param>
        /// <returns></returns>
        protected bool TryClosePODL(int idxPODL)
        {
            bool fatto = false;
            string fullUrl = $"{urlPODLClose}{idxPODL}";
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    // invio chiamata URL x chiusura ODL su macchina
                    string callResp = await callUrl(fullUrl, false);
                    fatto = (callResp != "KO") ? true : false;
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TryClosePODL | idxPODL: {idxPODL}{Environment.NewLine}{ex.Message}");
            }
            lgInfo($"Eseguito TryClosePODL per {idxPODL} | url: {fullUrl} | esito: {fatto}");
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per tentare chiusura PODL --&gt; ODL specifico
        /// </summary>
        /// <param name="idxPODL"></param>
        /// <param name="doConfirm"></param>
        /// <param name="dtEve"></param>
        /// <param name="dtCurr"></param>
        /// <returns></returns>
        protected bool TryClosePODL(int idxPODL, DateTime dtEve, DateTime dtCurr)
        {
            bool fatto = false;
            string fullUrl = $"{urlPODLClose}{idxPODL}&dtEve={dtEve:yyyyMMddHHmmssfff}&dtCurr={dtCurr:yyyyMMddHHmmssfff}";
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    // invio chiamata URL x chiusura ODL su macchina
                    string callResp = await callUrl(fullUrl, false);
                    fatto = (callResp != "KO") ? true : false;
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TryClosePODL | idxPODL: {idxPODL} | dtEve: {dtEve} | dtCurr: {dtCurr}{Environment.NewLine}{ex.Message}");
            }
            lgInfo($"Eseguito TryClosePODL per {idxPODL} | url: {fullUrl} | esito: {fatto}");
            return fatto;
        }

        /// <summary>
        /// Processa avvio PODL (ed eventuale chiusura precedente)
        /// </summary>
        /// <param name="codArt">Cod Articolo del PODL da avviare</param>
        /// <param name="codGruppo">Cod Gruppo dell'impianto come default</param>
        /// <param name="numPz">num pz richiesti</param>
        protected int TryCreatePodl(string codArt, string codGruppo, int numPz)
        {
            int answ = 0;
            string urlEncoded = $"{urlCreatePOdl}CodArt={codArt}&CodGruppo={codGruppo}&numPz={numPz}";
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    string resp = await callUrl(urlEncoded, false);
                    int.TryParse(resp, out answ);
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TryCreatePodl | codArt: {codArt} | codGruppo: {codGruppo} | numPz: {numPz}{Environment.NewLine}{ex.Message}");
            }
            return answ;
        }

        /// <summary>
        ///  Effettua verifica se abilitato invio pezzi in blocco PER TAVOLE e nel caso
        /// - invio in blocco pezzi
        /// - aggiornamento del contapezzi (passato come ref) x nuovo valore post invio
        /// </summary>
        /// <param name="fullCode">Idx macchina completo, con tavola/pallet di invio</param>
        /// <param name="pzCountMes">Contapezzi MES (IOB) attuale</param>
        /// <param name="pzCountImp">Contapezzi impianto (per la tavola indicata)</param>
        protected virtual int trySendPzCountBlock(string fullCode, int pzCountMes, int pzCountImp)
        {
            int qtyAdded = 0;
            lgDebug($"Chiamata trySendPzCountBlock MULTI | fullCode: {fullCode} | pzCountMes: {pzCountMes} | pzCountImp: {pzCountImp}");
            // in primis HA SENSO procedere SOLO SE server MP è Online...
            if (MPOnline)
            {
                // SOLO SE online la macchina...
                if (IobOnline)
                {
                    int numIncr = 0;
                    // verifico se la funzione SIA abilitata
                    if (enableSendPzCountBlock)
                    {
                        int delta = pzCountImp - pzCountMes;
                        // se è abilitata verifico differenza: se ho DELTA > minSendPzCountBlock -->
                        // invio un blocco <= maxSendPzCountBlock
                        if (delta > minSendPzCountBlock)
                        {
                            // init genObj display
                            newDisplayData currDispData = new newDisplayData();
                            // resta indietro di ALMENO minSendPzCountBlock pezzi x recuperare 1:1...
                            numIncr = delta > maxSendPzCountBlock + minSendPzCountBlock ? maxSendPzCountBlock : delta - minSendPzCountBlock;
                            // invio il num max di pezzi ammesso in blocco!
                            lastUrl = $"{urlAddPzCount}{numIncr}".Replace(IOBConfFull.General.CodIOB, fullCode);
                            string resp = utils.CallUrlGet(lastUrl);
                            if (!string.IsNullOrEmpty(resp))
                            {
                                // dalla risposta (come numero) capisco SE ha aggiunto i pezzi (e quanti)
                                int.TryParse(resp, out qtyAdded);
                                if (qtyAdded > 0)
                                {
                                    // aggiorno contapezzi ...
                                    pzCountMes += qtyAdded;
                                    lgInfo($"SEND incremento contapezzi: send: {numIncr} | resp: {qtyAdded} | contapezziMES: {pzCountMes}");
                                    // invio conferma contapezzi..
                                    string fullUrl = $"{urlSetPzCount}{pzCountMes}".Replace(IOBConfFull.General.CodIOB, fullCode);
                                    string retVal = utils.callUrl(fullUrl);
                                    // verifica se tutto OK
                                    if (retVal != $"{pzCountMes}")
                                    {
                                        // errore salvataggio contapezzi
                                        lgError($"trySendPzCountBlock: errore salvataggio contapezzi: contapezziMES {pzCountMes} | risposta: {retVal}");
                                    }
                                }
                                else
                                {
                                    lgError($"Richiesto incremento {numIncr} ma NON registrato su server MP-IO");
                                }
                            }
                            currDispData.newUrlCallData = lastUrl;
                            currDispData.counter = pzCountMes;
                            currDispData.semOut = Semaforo.SV;
                            raiseRefresh(currDispData);
                        }
                    }
                }
                else
                {
                    lgError("Impossibile trySendPzCountBlock: IobOnline è false");
                }
            }
            else
            {
                lgError("Impossibile trySendPzCountBlock: MPOnline è false");
            }
            return qtyAdded;
        }

        /// <summary>
        ///  Effettua verifica se abilitato invio pezzi in blocco (caso macchina standard/singolo contapezzi) e nel caso
        /// - invio in blocco pezzi
        /// - aggiornamento del contapezzi (passato come ref) x nuovo valore post invio
        /// </summary>
        protected virtual void trySendPzCountBlock()
        {
            lgDebug($"Chiamata trySendPzCountBlock STD | pzCountMes: {contapezziIOB} | pzCountImp: {contapezziPLC}");
            // in primis HA SENSO procedere SOLO SE server MP è Online...
            if (MPOnline)
            {
                // SOLO SE online la macchina...
                if (IobOnline)
                {
                    int numIncr = 0;
                    int qtyAdded = 0;
                    // verifico se la funzione SIA abilitata
                    if (enableSendPzCountBlock)
                    {
                        int delta = contapezziPLC - contapezziIOB;
                        // se è abilitata verifico differenza: se ho DELTA > minSendPzCountBlock -->
                        // invio un blocco <= maxSendPzCountBlock
                        if (delta > minSendPzCountBlock)
                        {
                            // init genObj display
                            newDisplayData currDispData = new newDisplayData();
                            // resta indietro di ALMENO minSendPzCountBlock pezzi x recuperare 1:1...
                            numIncr = delta > maxSendPzCountBlock + minSendPzCountBlock ? maxSendPzCountBlock : delta - minSendPzCountBlock;
                            // invio il num max di pezzi ammesso in blocco!
                            lastUrl = $"{urlAddPzCount}{numIncr}";
                            string resp = utils.CallUrlGet(lastUrl);
                            if (!string.IsNullOrEmpty(resp))
                            {
                                // dalla risposta (come numero) capisco SE ha aggiunto i pezzi (e quanti)
                                int.TryParse(resp, out qtyAdded);
                                if (qtyAdded > 0)
                                {
                                    // aggiorno IL MIO contapezzi...
                                    contapezziIOB += qtyAdded;
                                    lgInfo($"SEND incremento contapezzi: send: {numIncr} | resp: {qtyAdded} | contapezziIOB: {contapezziIOB}");
                                    // invio conferma contapezzi..
                                    string fullUrl = $"{urlSetPzCount}{contapezziIOB}";
                                    string retVal = utils.callUrl(fullUrl);
                                    // verifica se tutto OK
                                    if (retVal != contapezziIOB.ToString())
                                    {
                                        // errore salvataggio contapezzi
                                        lgError($"trySendPzCountBlock: errore salvataggio contapezzi: contapezziIOB {contapezziIOB} | risposta: {retVal}");
                                    }
                                }
                                else
                                {
                                    lgError($"Richiesto incremento {numIncr} ma NON registrato su server MP-IO");
                                }
                            }
                            currDispData.newUrlCallData = lastUrl;
                            currDispData.counter = contapezziIOB;
                            currDispData.semOut = Semaforo.SV;
                            raiseRefresh(currDispData);
                        }
                    }
                }
                else
                {
                    lgError("Impossibile trySendPzCountBlock: IobOnline è false");
                }
            }
            else
            {
                lgError("Impossibile trySendPzCountBlock: MPOnline è false");
            }
        }

        /// <summary>
        /// Cerca di inviare su un altro thread i vari dati accumulati...
        /// </summary>
        protected async Task TrySendValuesAsync()
        {
            // init genObj display
            newDisplayData currDispData = new newDisplayData();
            try
            {
                // verifico se risponde il server...
                bool srvAlive = await CheckServerAliveAsync();
                if (srvAlive)
                {
                    bool iobOk = false;
                    iobOk = await CheckIobEnabled();
                    // verifico SE posso inviare dati
                    if (iobOk)
                    {
                        currDispData.semOut = Semaforo.SV;
                        // gestione queue SignalIN (invio, display)
                        await svuotaCodaSignInAsync();
                        currDispData.counter = contapezziIOB;
                        raiseRefresh(currDispData);
                        // provo a svuotare coda contapezzi
                        await SvuotaCodaContapezziAsync();
                        currDispData.counter = contapezziIOB;
                        raiseRefresh(currDispData);
                        // gestione queue FluxLog (invio, display)
                        await SvuotaCodaFLogAsync();
                        // coda RawTransf
                        await SvuotaCodaRawTransfAsync();
                        // coda UserLog
                        await SvuotaCodaULogAsync();
                        // refresh
                        raiseRefresh(currDispData);
                        // se arrivo è tutto ok...
                        currSendErrors = 0;
                    }
                    else
                    {
                        // mostro VETO-SEND x invio... GIALLO
                        currDispData.semOut = Semaforo.SG;
                        if (periodicLog)
                        {
                            lgInfo("IOB - VETO SEND");
                        }
                    }
                }
                else
                {
                    // mostro SERVER KO x invio... ROSSO
                    currDispData.semOut = Semaforo.SR;
                    if (periodicLog)
                    {
                        lgWarn("IOB | TrySendValuesAsync.CheckServerAliveAsync | SERVER NOT READY");
                    }
                }
            }
            catch (Exception exc)
            {
                lgError($"Errore in fase TrySendValuesAsync | currSendErrors: {currSendErrors}{Environment.NewLine}{exc}");
                currDispData.semOut = Semaforo.SR;
            }
            raiseRefresh(currDispData);
        }

        /// <summary>
        /// Versione sync chiamata setup PODL
        /// </summary>
        /// <param name="idxPODL"></param>
        /// <returns></returns>
        protected bool TrySetupPODL(int idxPODL)
        {
            bool fatto = false;
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    fatto = await TrySetupPODLAsync(idxPODL);
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in TrySetupPODL | idxPODL: {idxPODL}{Environment.NewLine}{ex.Message}");
            }
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per tentare setup del PODL indicato
        /// </summary>
        /// <param name="idxPODL"></param>
        /// <returns></returns>
        protected async Task<bool> TrySetupPODLAsync(int idxPODL)
        {
            bool fatto = false;
            string fullUrl = $"{urlOdlStartFromPOdl}{idxPODL}";
            try
            {
                // invio chiamata URL x avvio PODL su macchina
                string rawSplit = await callUrl(fullUrl, false);
                fatto = (rawSplit != "KO") ? true : false;
            }
            catch
            { }
            return fatto;
        }

        /// <summary>
        /// Effettua chiamata MP-IO per tentare setup del PODL indicato con indicazioni estese
        /// (confirm pezzi, dtEvento, dtCorrente)
        /// </summary>
        /// <param name="idxPODL"></param>
        /// <param name="doConfirm"></param>
        /// <param name="dtEve"></param>
        /// <param name="dtCurr"></param>
        /// <returns></returns>
        protected async Task<bool> TrySetupPODLAsync(int idxPODL, bool doConfirm, DateTime dtEve, DateTime dtCurr)
        {
            bool fatto = false;
            //string format = "yyyyMMddHHmmssfff";
            string fullUrl = $"{urlOdlStartFromPOdl}{idxPODL}&doConfirm={doConfirm}&dtEve={dtEve:yyyyMMddHHmmssfff}&dtCurr={dtCurr:yyyyMMddHHmmssfff}";
            try
            {
                // invio chiamata URL x avvio PODL su macchina
                string rawSplit = await callUrl(fullUrl, false);
                fatto = (rawSplit != "KO") ? true : false;
            }
            catch
            { }
            return fatto;
        }

        /// <summary>
        /// URL del comando letto da conf in aggiunta al server MES da contattare
        /// </summary>
        protected string urlCommand(string cmqReq)
        {
            return $@"{urlMesServer}{IOBConfFull.MapoMes.ApiUrl(cmqReq)}";
        }

        /// <summary>
        /// URL come urlCommand + aggiunta codice IOB da cui viene inviato
        /// </summary>
        protected string urlCommandIob(string cmqReq)
        {
            return $@"{urlMesServer}{IOBConfFull.MapoMes.ApiUrl(cmqReq)}{IOBConfFull.General.CodIOB}";
        }

        /// <summary>
        /// URL come urlCommand + aggiunta codice IOB da file (= specifico x Reboot) da cui viene inviato
        /// </summary>
        protected string urlCommandIobFile(string cmqReq)
        {
            return $@"{urlMesServer}{IOBConfFull.MapoMes.ApiUrl(cmqReq)}{IOBConfFull.General.FilenameIOB}";
        }

        /// <summary>
        /// URL per richiamo task da eseguire...
        /// </summary>
        protected string urlRemTask2ExeTav(string codTav)
        {
            return $@"{urlCommandIob("remTask2Exe")}|{codTav}?taskName=";
        }

        /// <summary>
        /// Verifica se il parametro passi il limite della DeadBand (globale o specifica se
        /// configurata) Il riferimento è al prec valore currProdData
        /// </summary>
        /// <param name="keyName">nome del parametro da verificare</param>
        /// <param name="actVal">valore attuale da testare</param>
        /// <returns>
        /// true = passa controllo, è da inviare / false = variazione entro deadband non da inviare
        /// </returns>
        protected virtual bool valueOverDBand(string keyName, string actVal)
        {
            bool answ = true;
            float oldVal = 0;
            float newVal = 0;
            // DEVE esserci un valore global deadband > 0...
            if (GLOBAL_DBAND > 0)
            {
                // in primis deve essere in currProdData
                if (currProdData.ContainsKey(keyName))
                {
                    // se c'è DEVE passare il test decodifica float...
                    bool isFloatOld = float.TryParse(currProdData[keyName], out oldVal);
                    bool isFloatNew = float.TryParse(actVal, out newVal);
                    if (isFloatOld && isFloatNew)
                    {
                        // in questo caso verifico la differenza sia superiore alla deadband
                        answ = Math.Abs(newVal - oldVal) > GLOBAL_DBAND;
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera da conf durata del veto x FluxLog (default 60sec)
        /// </summary>
        /// <param name="codFlux"></param>
        /// <returns></returns>
        protected int vetoCodFluxDur(string codFlux)
        {
            int answ = 60;
            if (memMap != null && memMap.mMapRead != null && memMap.mMapRead.Count > 0)
                if (memMap.mMapRead.ContainsKey(codFlux))
                {
                    answ = memMap.mMapRead[codFlux].period;
                }
            return answ;
        }

        #endregion Protected Methods

        #region Private Fields

        private static readonly JsonSerializerOptions options = new JsonSerializerOptions
        {
            ReferenceHandler = ReferenceHandler.IgnoreCycles,
            WriteIndented = false // Optional: set to true for pretty-printing
        };

        /// <summary>
        /// Oggetto logger della classe
        /// </summary>
        private static Logger _logger = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// Accumulatore counter byte x inviare meno record in REDIS
        /// </summary>
        private long currByteCount = 0;

        /// <summary>
        /// Dizionario dei valori FluxLog filtrati
        /// </summary>
        private Dictionary<string, long> DictFiltFLog = new Dictionary<string, long>();

        /// <summary>
        /// Ultima data x statistiche daily x trackDynData
        /// </summary>
        private string LastDayCurr = "";

        /// <summary>
        /// Ultimo invio valore a server
        /// </summary>
        private DateTime lastSigVarSent = DateTime.Now;

        /// <summary>
        /// periodo minimo di veto invio dati qualora non siano variati
        /// </summary>
        private int lastSigVarVeto = 55;

        private Dictionary<string, int> TrackDayStatsCount = new Dictionary<string, int>();
        private Dictionary<string, int> TrackDetStatsCount = new Dictionary<string, int>();
        private Dictionary<string, Dictionary<string, string>> TrackDetValsCount = new Dictionary<string, Dictionary<string, string>>();
        private DateTime vetoCheckOdl = DateTime.Now;

        /// <summary>
        /// Valore veto al salvataggio di valori filtrati in FluxLog se NON superato limite chiamate
        /// </summary>
        private DateTime VetoFlushFiltFL = DateTime.Now.AddHours(1);

        /// <summary>
        /// Dizionario dei valori bloccati x evitare log eccessivo
        /// </summary>
        private Dictionary<string, DateTime> vetoLogError = new Dictionary<string, DateTime>();

        /// <summary>
        /// Periodo di veto log in minuti
        /// </summary>
        private int vetoPeriodMin = 15;

        /// <summary>
        /// Periodo Veto prima di eseguire ProcessAutoOdlAsync
        /// </summary>
        private DateTime VetoProcessAutoOdl = DateTime.Now;

        /// <summary>
        /// Variabile x vietare rilettura conf memoria (se è stato letto metto veto lungo ad 10 min x evitare loop in avvio)
        /// </summary>
        private DateTime vetoReloadConf = DateTime.Now.AddMinutes(-1);

        /// <summary>
        /// Dizionario dei valori bloccati x evitare log basato su veto temporale
        /// </summary>
        private Dictionary<string, DateTime> vetoSendFLog = new Dictionary<string, DateTime>();

        /// <summary>
        /// Ms di attesa x uscita processo (std)
        /// </summary>
        private int waitForExitMsec = 250;

        #endregion Private Fields

        #region Private Properties

        /// <summary>
        /// Boolean abilitazione coda eventi IN
        /// </summary>
        private bool qInEnabCurr { get; set; } = false;

        /// <summary>
        /// Redis key del dizionari valori currProdData persistiti
        /// </summary>
        private string rKeyCurrProdData
        {
            get => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CurrProdData");
        }

        /// <summary>
        /// URL di base del server MES da contattare
        /// </summary>
        private string urlMesServer
        {
            get => $@"{IOBConfFull.MapoMes.Transport}://{IOBConfFull.MapoMes.IpAddr}";
        }

        #endregion Private Properties

        #region Private Methods

        /// <summary>
        /// Aggiunge in setup memoria la checkCondition BIT "decodificata"
        /// </summary>
        /// <param name="ompKey">Chiave da aggiungere</param>
        /// <param name="ompVal">Valore da decodificare e poi aggiungere</param>
        private void addCheckConditionBit(string ompKey, string ompVal)
        {
            var newCond = new BitConditionCheck(ompKey, ompVal);
            // cerco x aggiornare o aggiungere...
            if (OptCheckCondBit.ContainsKey(ompKey))
            {
                OptCheckCondBit[ompKey] = newCond;
            }
            else
            {
                OptCheckCondBit.Add(ompKey, newCond);
            }
        }

        /// <summary>
        /// Aggiunge in setup memoria la checkCondition INT "decodificata"
        /// </summary>
        /// <param name="ompKey">Chiave da aggiungere</param>
        /// <param name="ompVal">Valore da decodificare e poi aggiungere</param>
        private void addCheckConditionInt(string ompKey, string ompVal)
        {
            var newCond = new IntConditionCheck(ompKey, ompVal);
            // cerco x aggiornare o aggiungere...
            if (OptCheckCondInt.ContainsKey(ompKey))
            {
                OptCheckCondInt[ompKey] = newCond;
            }
            else
            {
                OptCheckCondInt.Add(ompKey, newCond);
            }
        }

        /// <summary>
        /// Aggiunge in setup memoria l'oggetto tipo valore BIT (NON mutuamente esclusivo) da tradurre
        /// </summary>
        /// <param name="csvList">Elenco valori da aggiungere formato csv (es "a,b,c")</param>
        private void addVal2TranslBit(string csvList)
        {
            var list2add = csvList.Split(',');
            foreach (var memKey in list2add)
            {
                // cerco x aggiungere...
                if (!OptVar2TranslBit.ContainsKey(memKey))
                {
                    // init traduzione vuota
                    OptVar2TranslBit.Add(memKey, "");
                }
            }
        }

        /// <summary>
        /// Aggiunge in setup memoria l'oggetto tipo valore INT (mutuamente esclusivo) da tradurre
        /// </summary>
        /// <param name="csvList">Elenco valori da aggiungere formato csv (es "a,b,c")</param>
        private void addVal2TranslInt(string csvList)
        {
            var list2add = csvList.Split(',');
            foreach (var memKey in list2add)
            {
                // cerco x aggiungere...
                if (!OptVar2TranslInt.ContainsKey(memKey))
                {
                    // init traduzione vuota
                    OptVar2TranslInt.Add(memKey, "");
                }
            }
        }

        /// <summary>
        /// Verifica e se necessario comprime directory log...
        /// </summary>
        private void checkShrinkDir()
        {
            // comprimo x prima cosa la folder dell'IOB corrente... testo sia IOBConf che FilenameIOB
            List<string> path2chk = new List<string>() { IOBConfFull.General.CodIOB, IOBConfFull.General.FilenameIOB, "MAIN" };
            string fullPath = "";
            foreach (var item in path2chk)
            {
                fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", item);
                try
                {
                    baseUtils.shrinkDir(fullPath);
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in checkShrinkDir{Environment.NewLine}{exc}");
                }
            }
        }

        /// <summary>
        /// retituisce data-ora dell'ODL corrente
        /// </summary>
        /// <returns></returns>
        private async Task<DateTime> currOdlStart()
        {
            DateTime inizioOdl = DateTime.Now;
            string rawDataInizio = await callUrl(urlInizioOdlIob, false);
            DateTime.TryParse(rawDataInizio, out inizioOdl);
            return inizioOdl;
        }

        /// <summary>
        /// Mostra i dati grezzi letti in esadecimale <paramref name="currDispData">Parametri da
        /// aggiornare x display in form</paramref>
        /// </summary>
        private void displayRawData(ref newDisplayData currDispData)
        {
            // mostro update...
            string newString = string.Format("{0:X}", B_input);
            currDispData.newInData = newString;
        }

        /// <summary>
        /// Accumula statistiche daily e di dettaglio x la chiave indicata
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        private void DoTrackDataCount(string key, string value)
        {
            if (TrackDayStatsCount.ContainsKey(key))
            {
                TrackDayStatsCount[key]++;
            }
            else
            {
                TrackDayStatsCount.Add(key, 1);
            }
            // dettaglio è insieme key_val
            string detKey = $"{key}:{value}";
            if (TrackDayStatsCount.ContainsKey(detKey))
            {
                TrackDayStatsCount[detKey]++;
            }
            else
            {
                TrackDayStatsCount.Add(detKey, 1);
            }
            // salvo anche dettaglio...
            if (TrackDetValsCount.ContainsKey(key))
            {
                TrackDetValsCount[key].Add($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}", value);
            }
            else
            {
                Dictionary<string, string> nDict = new Dictionary<string, string>();
                nDict.Add($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}", value);
                TrackDetValsCount.Add(key, nDict);
            }
        }

        /// <summary>
        /// Salva le statistiche accumulate alla giornata di riferimento salvata e resetta contatori
        /// </summary>
        /// <param name="DayCurr"></param>
        private void DoTrackDataSave()
        {
            string rKey = "";
            // verifico se ho almeno num minimo di dati da tracciare...
            if (TrackDayStatsCount.Count() > 0)
            {
                // cerco se ho qualcosa oltre soglia...
                int numOver = TrackDayStatsCount.Where(x => x.Value > IOBConfFull.FluxLog.TrackDataThreshold).Count();
                // se ho --> processo!
                if (numOver > 0)
                {
                    // scadenza: 1 mese
                    DateTime scadHash = DateTime.Today.AddMonths(1);
                    // salvo statistiche Day
                    rKey = $"{redisMan.redIobTrackKey}:DayStats:{LastDayCurr}";
                    foreach (var item in TrackDayStatsCount)
                    {
                        // traccio con scadenza 1 mese da oggi pareto chiamate giornaliere
                        redisMan.redIncrHashCount(rKey, item.Key, scadHash, item.Value);
                    }
                    // salvo statistiche Detail, scadenza 10 gg
                    scadHash = DateTime.Today.AddDays(10);
                    foreach (var item in TrackDetStatsCount)
                    {
                        // separo statistiche...
                        var kvp = item.Key.Split(':');
                        if (kvp.Count() > 1)
                        {
                            rKey = $"{redisMan.redIobTrackKey}:DetailStats:{LastDayCurr}:{kvp[0]}";
                            redisMan.redIncrHashCount(rKey, kvp[1], scadHash, item.Value);
                        }
                    }
                    // salvo dizionario valori dettaglio... scadenza 3 gg
                    scadHash = DateTime.Today.AddDays(3);
                    foreach (var item in TrackDetValsCount)
                    {
                        rKey = $"{redisMan.redIobTrackKey}:DataLog:{LastDayCurr}:{item.Key}";
                        redisMan.redSaveHashDict(rKey, item.Value, scadHash);
                    }
                    // reset dizionari
                    TrackDayStatsCount.Clear();
                    TrackDetStatsCount.Clear();
                    TrackDetValsCount.Clear();
                }
            }
            // salvo variabile giorno di registrazione
            LastDayCurr = $"{DateTime.Today:yyMMdd}";
        }

        /// <summary>
        /// Archivia una cartella in un file zip
        /// </summary>
        /// <param name="folderPath">Cartelal da archiviare</param>
        /// <param name="zipName">Nome del file zip da produrre...</param>
        private bool doZipArchiveFolder(string folderPath, string zipName)
        {
            bool fatto = false;
            if (Directory.Exists(folderPath))
            {
                fatto = fileMover.zippaDirectory(folderPath, zipName);
                // se sent elimino vecchia directory...
                if (fatto)
                {
                    Directory.Delete(folderPath, true);
                }
            }
            return fatto;
        }

        private async Task<bool> ExecuteIobCheckWithRetry(int maxRetries)
        {
            var rand = new Random();
            for (int i = 0; i <= maxRetries; i++)
            {
                try
                {
                    if (i > 0)
                    {
                        // Al terzo tentativo fallito resetto i client
                        if (i == 3) resetWebClients();

                        int delay = i == 3 ? rand.Next(250, 1000) : rand.Next(250, 500);
                        await Task.Delay(delay);
                    }

                    string callResp = await callUrl(urlIobEnabled, i < 3); // true per i primi tentativi
                    if (callResp == "OK") return true;
                }
                catch (Exception exc)
                {
                    lgError($"Eccez<ione in ExecuteIobCheckWithRetry{Environment.NewLine}{exc}");
                }
            }
            return false;
        }

        /// <summary>
        /// Esegue filtraggio dati x bit blinking!!!
        /// </summary>
        private void filterData()
        {
            // effettuo filtraggio dei valori letti... inizializzo OUT!
            B_output = 0;
            // in primis verifico SE ci siano bit blinkng... se non ci sono OUT=IN...
            if (IOBConfFull.SignalProc.BlinkFilterMask == 0)
            {
                B_output = B_input;
            }
            else
            {
                // incomincio con i valori NON blinking: questi "passano invariati", inizio a
                // sommare nel valore OUT...
                B_output = B_input & ~IOBConfFull.SignalProc.BlinkFilterMask;
                // calcolo il valore dei BIT che "passano la maschera"
                int iBlink = B_input & IOBConfFull.SignalProc.BlinkFilterMask;
                // ...aggiungo i "bit che passano"
                B_output += iBlink;

                // calcolo QUALI valori (tra quelli blink) siano PASSATI da 0 a 1 --> init counters...
                BitArray bBlinkStart = new BitArray(new byte[] { Convert.ToByte(iBlink) });
                int[] bitsUp = bBlinkStart.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                for (int i = 0; i < bitsUp.Length; i++)
                {
                    // SE 1... impostiamo contatori al MAX
                    if (bitsUp[i] == 1)
                    {
                        // se era zero indico START blink...
                        if (i_counters[i] == 0)
                        {
                            lgTrace("START BLINK: B{0}", i);
                        }
                        // imposto comunque contatore al cambio fronte...
                        i_counters[i] = IOBConfFull.SignalProc.BlinkMaxCounter;
                    }
                }

                // quelli che sono zero... LI RECUPERO E LI PROCESSO...
                int iZero = ~B_input & IOBConfFull.SignalProc.BlinkFilterMask;
                BitArray bBlinkEnd = new BitArray(new byte[] { Convert.ToByte(iZero) });
                int[] bitsDown = bBlinkEnd.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                for (int i = 0; i < bitsDown.Length; i++)
                {
                    // se era a zero (invertito...)
                    if (bitsDown[i] == 1)
                    {
                        // SE è in corso il conteggio...
                        if (i_counters[i] > 0)
                        {
                            // decremento!
                            i_counters[i] -= 1;
                            // se è zero NON faccio nulla, altrimenti SOMMO...
                            if (i_counters[i] > 0)
                            {
                                B_output += 1 << i;
                            }
                            else
                            {
                                lgTrace("END BLINK: B{0}", i);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Legge da conf il valore di demoltiplica lettura dynData (se presente) o ignora e pone a 1...
        /// </summary>
        private void fixDemFactDynData()
        {
            demFactDynData = IOBConfFull.FluxLog.DemFactDynData;
        }

        /// <summary>
        /// Test ping all'indirizzo impostato nei parametri
        /// </summary>
        /// <returns></returns>
        private IPStatus GetPingStatus()
        {
            var pStatus = Task.Run(async () => await GetPingStatusAsync(maxPingRetry + 1))
                               .GetAwaiter()
                               .GetResult();

            return pStatus;
        }

        /// <summary>
        /// Test ping all'indirizzo impostato nei parametri
        /// </summary>
        /// <param name="maxRetry">Numero max tentativi (maxPingRetry + 1)</param>
        /// <returns></returns>
        private async Task<IPStatus> GetPingStatusAsync(int maxAttempts)
        {
            // 1. Check disabilitazione
            if (IOBConfFull.MapoMes.DisabPing) return IPStatus.Success;

            // 2. Estrazione Host/IP pulita
            string host = IOBConfFull.MapoMes.IpAddr;
            try
            {
                // Rimuove protocollo (http://, ftp://, etc)
                if (host.Contains("://"))
                    host = new Uri(host).Host;
                else if (host.Contains(":"))
                    host = host.Split(':')[0];
            }
            catch { /* fallback al valore originale se Uri fallisce */ }

            // 3. Risoluzione Indirizzo
            if (!IPAddress.TryParse(host, out IPAddress address))
            {
                try
                {
                    var addresses = Dns.GetHostAddresses(host);
                    if (addresses.Length > 0) address = addresses[0];
                }
                catch (Exception ex)
                {
                    lgError($"Impossibile risolvere DNS per {host}: {ex.Message}");
                    return IPStatus.DestinationHostUnreachable;
                }
            }

            if (address == null) return IPStatus.Unknown;

            // 4. Ciclo di Ping con Retry
            var rand = new Random();
            using (Ping pingSender = new Ping())
            {
                for (int attempt = 1; attempt <= maxAttempts; attempt++)
                {
                    try
                    {
                        // Timeout dinamico come nel tuo originale
                        int timeout = (attempt == 1)
                            ? rand.Next(200, 400)
                            : (pingServerMsTimeout * attempt / 2);

                        PingReply reply = pingSender.Send(address, timeout);

                        if (reply.Status == IPStatus.Success)
                        {
                            if (attempt > 1) lgInfo("Server PING OK dopo retry!");
                            return IPStatus.Success;
                        }

                        lgInfo($"Ping tent. {attempt} fallito: {reply.Status} (Timeout: {timeout}ms)");
                    }
                    catch (Exception ex)
                    {
                        lgError($"Eccezione durante ping tent. {attempt}: {ex.Message}");
                    }

                    // Attesa prima del prossimo tentativo (se non è l'ultimo)
                    if (attempt < maxAttempts)
                    {
                        await Task.Delay(rand.Next(50, 200));
                    }
                }
            }

            return IPStatus.TimedOut;
        }

        /// <summary>
        /// Verifica se il log di un dato errore sia permesso
        /// </summary>
        /// <param name="logKey">ID del valore log da loggare/verificare</param>
        /// <returns></returns>
        private bool logValuePermit(string logKey)
        {
            bool doLog = false;
            if (vetoLogError.ContainsKey(logKey))
            {
                // verifico se veto scaduto...
                if (DateTime.Now > vetoLogError[logKey])
                {
                    doLog = true;
                    vetoLogError[logKey] = DateTime.Now.AddMinutes(vetoPeriodMin);
                }
            }
            else
            {
                doLog = true;
                vetoLogError.Add(logKey, DateTime.Now.AddMinutes(vetoPeriodMin));
            }

            return doLog;
        }

        /// <summary>
        /// Classe fittizia in caso di processing GLOBALE di tutto in 1 solo colpo...
        /// </summary>
        private void processAllMemory()
        {
            // verifico se sia abilitato processing...
            if (queueInEnabCurr)
            {
                // init genObj display
                newDisplayData currDispData = new newDisplayData();
                // in primis SALVO valori previous/precedenti
                B_previous = B_output;
                // poi faccio lettura NUOVI valori
                readAllData(ref currDispData);
                // eseguo il filtering dei valori (per i bit "blinking")
                filterData();
                // effettuo confronto valori vecchi/nuovi... SE trovo variazione OPPURE se è passato
                // + di un timeout di controllo...
                DateTime adesso = DateTime.Now;
                bool scaduto = Math.Abs(adesso.Subtract(lastSigVarSent).TotalSeconds) > lastSigVarVeto;
                if (B_output != B_previous || (scaduto && !IOBConfFull.Device.DisabResendScaduto))
                {
                    lgDebug($"processAllMemory | B_output: {B_output} | B_previous: {B_previous}");
                    accodaSigIN(ref currDispData);
                    lastSigVarSent = adesso;
                }
                else
                {
                    lgTrace($"processAllMemory | scaduto: {scaduto} | lastSigVarSent: {lastSigVarSent} | B_output: {B_output} | B_previous: {B_previous}");
                }
                raiseRefresh(currDispData);
            }
            else
            {
                lgInfo($"VETO on processAllMemory | queueInEnabCurr = {queueInEnabCurr}");
                checkVetoQueueIn();
            }
        }

        private void processMem2Write()
        {
            // solo SE queue in enabled è true...
            if (queueInEnabCurr)
            {
                DateTime adesso = DateTime.Now;
                List<objItem> updatedPar = new List<objItem>();
                List<objItem> currWritePar = new List<objItem>();
                // ciclo tutti gli oggetti write x vedere se modificati...
                foreach (var item in currProdData)
                {
                    bool needWrite = false;
                    string lastVal = item.Value;
                    // li cerco su last... se non ci sono o modificati --> salvo da scrivere e copio
                    if (lastProdData.ContainsKey(item.Key))
                    {
                        lastVal = lastProdData[item.Key];
                        // verifico se variato...
                        if (!item.Value.Equals(lastProdData[item.Key]))
                        {
                            needWrite = true;
                            lastProdData[item.Key] = item.Value;
                        }
                    }
                    // aggiungo
                    else
                    {
                        needWrite = true;
                        lastProdData.Add(item.Key, item.Value);
                    }
                    if (memMap != null && memMap.mMapWrite.ContainsKey(item.Key))
                    {
                        // preparo genObj da scrivere
                        objItem newWrite = new objItem()
                        {
                            uid = item.Key,
                            name = item.Key,
                            description = memMap.mMapWrite[item.Key].description,
                            reqValue = needWrite ? item.Value : "",
                            value = lastVal, //item.Value,
                            lastRequest = adesso,
                            writable = true,
                            displOrdinal = memMap.mMapWrite[item.Key].displOrdinal
                        };

                        // se devo scrivere --> riporto
                        if (needWrite)
                        {
                            updatedPar.Add(newWrite);
                        }
                        else
                        {
                            currWritePar.Add(newWrite);
                        }
                    }
                }
                // se ho da scrivere... scrivo TUTTI!
                if (updatedPar.Count > 0 && ENABLE_MEM_REWRITE)
                {
                    // scrivo valore!
                    lgInfo($"Chiamata di plcWriteParams da processMem2Write: {updatedPar.Count} updatedPar");
                    plcWriteParams(ref updatedPar);
                    // invio su cloud parametri!
                    string rawData = JsonConvert.SerializeObject(updatedPar);
                    utils.CallUrlPost($"{urlUpdateWriteParams}", rawData);
                    lgInfo($"Notificato a server scrittura {updatedPar.Count} parametri");
                }
                else
                {
                    // se scaduto tempo da ultimo invio... e ho valori
                    if (currWritePar.Count > 0 && (lastWriteParamsUpsert.AddMinutes(vetoSendWriteUpsert) < adesso))
                    {
                        // invio su cloud parametri!
                        string rawData = JsonConvert.SerializeObject(currWritePar);
                        var res = utils.CallUrlPost($"{urlUpdateWriteParams}", rawData);
                        lgInfo($"Reinviato a server stato {updatedPar.Count} parametri WRITE");
                        lastWriteParamsUpsert = adesso;
                    }
                }
            }
        }

        /// <summary>
        /// Effettua gestioen programma: legge e mostra su display...
        /// </summary>
        private void processProgram()
        {
            currPrgName = "";
            // se abilitata lettura prgName
            if (enablePrgName)
            {
                if (connectionOk)
                {
                    try
                    {
                        currPrgName = getPrgName();
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in getPrgName{Environment.NewLine}{exc}");
                    }
                }
                else
                {
                    lgError("Errore connessione mancante x getPrgName");
                }
            }
            else
            {
                currPrgName = lastPrgName;
            }
            // verifico SE sia cambiato il programma...
            if (lastPrgName != currPrgName)
            {
                // salvo!
                lastPrgName = currPrgName;
                string sVal = $"[PROG]{currPrgName}";

                // chiamo accodamento...
                bool sent = accodaFLog("PROG", sVal, qEncodeFLog("PROG", currPrgName));
                if (sent)
                {
                    // traccio valore DynData x analisi
                    trackDynData("PROG", currPrgName);
                }
            }
        }

        /// <summary>
        /// Processo lettura dati sysinfo
        /// </summary>
        private void processSysInfo()
        {
            if (utils.CRB("enableSysInfo"))
            {
                Dictionary<string, string> currSysInfo = new Dictionary<string, string>();

                if (connectionOk)
                {
                    currSysInfo = getSysInfo();
                }
                else
                {
                    lgError("Errore connessione mancante x getSysInfo");
                }
                // verifico SE sia cambiato il programma...
                if (lastSysInfo != currSysInfo["SYSINFO"])
                {
                    // salvo!
                    lastSysInfo = currSysInfo["SYSINFO"];
                    // per ogni valore del dizionario mostro ed accodo!
                    string sVal = "";
                    foreach (var item in currSysInfo)
                    {
                        // verifico NON sia un ND...
                        if (item.Key == "ND" || string.IsNullOrEmpty(item.Key))
                        {
                            // log anomalia...
                            lgTrace($"Errore in processSysInfo: item.key risulta ND! | item.key: {item.Key} | item.Value: {item.Value}");
                        }
                        else
                        {
                            sVal = string.Format("[SYSINFO]{0}|{1}", item.Key, item.Value);
                            // chiamo accodamento...
                            bool sent = accodaFLog(item.Key, sVal, qEncodeFLog(item.Key, item.Value));
                            if (sent)
                            {
                                // traccio valore DynData x analisi
                                trackDynData(item.Key, item.Value);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Effettua calcolo dei consumi + esportazione in file richiesto
        /// </summary>
        /// <param name="folderPath">Folder dei file da processare</param>
        /// <param name="reportPath">Percorso file out di export</param>
        private bool RecipeDoConsumeReport(string folderPath, string reportPath)
        {
            // var di base
            bool fatto = false;
            string expMode = "csv";
            List<ConsRec> ListConsDet = new List<ConsRec>();
            List<ConsOut> ListConsSum = new List<ConsOut>();
            // file conf x conversioni...
            string confSetupPath = pathList["fullPath-confSetup"];
            ConvSetup currConf = new ConvSetup();
            bool addHeader = false;
            int numDec = 4;
            string codMag = "NA";
            if (!string.IsNullOrEmpty(confSetupPath))
            {
                string rawConfFile = File.ReadAllText(confSetupPath);
                if (!string.IsNullOrEmpty(rawConfFile))
                {
                    currConf = JsonConvert.DeserializeObject<ConvSetup>(rawConfFile);
                    if (currConf != null)
                    {
                        addHeader = currConf.addHeader;
                        numDec = currConf.numDec;
                        codMag = currConf.codMag;
                        expMode = currConf.mode.ToLower();
                    }
                }
            }
            // ciclo x ogni file ricevendone i consumi ed accumulandoli in lista...
            var fileList = Directory.GetFiles(folderPath);
            foreach (var cFile in fileList)
            {
                var consumiFile = RecipeGetCons(cFile);
                ListConsDet.AddRange(consumiFile);
            }
            // se ho trovato dati
            double ratioConv = 1;
            if (ListConsDet.Count > 0)
            {
                var dirName = new DirectoryInfo(folderPath).Name;
                // recupero elenco colori
                var elencoColori = ListConsDet
                                    .GroupBy(x => x.ColourCode)
                                    .Select(grp => grp.Last())
                                    .ToList();

                // faccio le somme x colore
                foreach (var colore in elencoColori)
                {
                    ratioConv = 1;
                    // ricerca in conf (% di consumo)
                    if (currConf != null && currConf.convRatio != null)
                    {
                        if (currConf.convRatio.ContainsKey(colore.ColourCode))
                        {
                            ratioConv = currConf.convRatio[colore.ColourCode];
                        }
                    }
                    // calcolo peso equivalente in kg con conversione ratio
                    var totWeightKg = ListConsDet
                                    .Where(x => x.ColourCode == colore.ColourCode)
                                    .Sum(x => x.WeightGrTot) / 1000 * ratioConv;

                    // record finale trasformato in KG...
                    ConsOut colTotal = new ConsOut()
                    {
                        CodMag = codMag,
                        ColourCode = colore.ColourCode,
                        Description = colore.Description,
                        UM = "KG",
                        WeightKgProc = totWeightKg > 0.01 ? Math.Round(totWeightKg, numDec) : 0.01,
                        DtRif = colore.DtRif
                    };
                    ListConsSum.Add(colTotal);
                }

                // infine serializzo x output in base alla modalità richiesta
                switch (expMode)
                {
                    case "fixwidth":
                        fatto = DataExport.SaveFixedWidth(ListConsSum, $"{reportPath}.txt", currConf.fieldLength, currConf.fieldLPad, currConf.fieldNDec);
                        break;

                    case "csv":
                    default:
                        fatto = DataExport.SaveToCsv(ListConsSum, $"{reportPath}.csv", addHeader);
                        break;
                }
            }
            return fatto;
        }

        /// <summary>
        /// Processa la ricetta alla ricerca dei dati PODL x inviare chiamate ad MP-IO
        /// </summary>
        /// <param name="recipeFile">Path RIcetta</param>
        private bool RecipeDoProcessPODL(string recipeFile)
        {
            bool answ = false;
            // init var
            int idxPOdl = 0;
            string rawVal = "";
            // leggo righe file!
            var recipeRows = File.ReadAllLines(recipeFile);
            // per ogni file ricevuto controlla se viene dal MES (cerca <Variant>PODL)
            string tokenSearch = "<Variant>PODL";
            // recupero riga PODL...
            var rowPodl = recipeRows.Where(x => x.Contains(tokenSearch)).FirstOrDefault();
            if (rowPodl != null)
            {
                DateTime dtStartPOdl = DateTime.Today;
                int duration = 0;
                DateTime dtEndPOdl = DateTime.Today.AddMinutes(10);
                string dtEve = "";
                string dtCurr = "";

                // inizio ricerca PODL
                rawVal = rowPodl.Trim().Replace(tokenSearch, "").Replace("</Variant>", "");
                int.TryParse(rawVal, out idxPOdl);
                if (idxPOdl > 0)
                {
                    // leggo inizio commessa
                    tokenSearch = "<Date>";
                    var rowStart = recipeRows.Where(x => x.Contains(tokenSearch)).FirstOrDefault();
                    if (rowStart != null)
                    {
                        rawVal = rowStart.Trim().Replace(tokenSearch, "").Replace("</Date>", "");
                        DateTime.TryParse(rawVal, out dtStartPOdl);
                    }

                    // leggo durata commessa
                    tokenSearch = "<DosDuration>";
                    var rowDurSec = recipeRows.Where(x => x.Contains(tokenSearch)).FirstOrDefault();
                    if (rowDurSec != null)
                    {
                        rawVal = rowDurSec.Trim().Replace(tokenSearch, "").Replace("</DosDuration>", "");
                        int.TryParse(rawVal, out duration);
                        dtEndPOdl = dtStartPOdl.AddSeconds(duration);
                    }

                    try
                    {
                        // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                        Task.Run(async () =>
                        {
                            // prova ad avviare/chiudere PODL relativo (eventualmente duplicandolo)
                            dtEve = $"{dtStartPOdl:yyyyMMddHHmmssfff}";
                            dtCurr = $"{DateTime.Now:yyyyMMddHHmmssfff}";
                            await callUrl($"{urlOdlStartFromPOdl}{idxPOdl}&dtEve={dtEve}&dtCurr={dtCurr}", false);

                            // ora chiamo chiusura...
                            dtEve = $"{dtEndPOdl:yyyyMMddHHmmssfff}";
                            dtCurr = $"{DateTime.Now:yyyyMMddHHmmssfff}";
                            await callUrl($"{urlPODLClose}{idxPOdl}&dtEve={dtEve}&dtCurr={dtCurr}", false);
                        })
                            .GetAwaiter()
                            .GetResult();
                    }
                    catch (Exception ex)
                    {
                        lgError($"Errore in RecipeDoProcessPODL.callUrl: {ex.Message}");
                    }
                }
                answ = true;
            }
            return answ;
        }

        /// <summary>
        /// Elimino record da REDIS (locale e remoto)
        /// </summary>
        /// <param name="keyReq"></param>
        private async Task RecipeRemoveWeekStatus(string keyReq)
        {
            string fullKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:WeekStats");
            var okHashDict = redisMan.redRemoveHashField(fullKey, keyReq);
            // rileggo status hash
            Dictionary<string, string> currDict = redisMan.redGetHashDict(fullKey);
            // invio ANCHE in MP-IO l'update delle info...
            string remUrl = urlSetHashDict;
            string dictPayload = JsonConvert.SerializeObject(currDict);
            await callUrlWithPayloadAsync(remUrl, dictPayload, true);
            //await callUrlWithPayloadAsync(remUrl, dictPayload, false);
        }

        private void reportDataProc()
        {
            // update valori visualizzazione...
            parentForm.dataProcLabel = string.Format("RAW: {0} --> IN: {1} --> OUT: {2}", nReadIN, nReadFilt, nSendOut);
        }

        /// <summary>
        /// Chiamate ritorno task eseguiti al server
        /// </summary>
        /// <param name="listaValori"></param>
        /// <returns></returns>
        private async Task SendTaskResult(List<string> listaValori)
        {
            foreach (var rawJob in listaValori)
            {
                // deserializzo...
                JobTaskData jobTask = JsonConvert.DeserializeObject<JobTaskData>(rawJob);
                // ora chiamo la cancellazione dei task eseguiti...
                var taskDict = JobTaskData.TaskDict(jobTask.RawData);
                foreach (var item in taskDict)
                {
                    await remTask2exe(item.Key, item.Value, jobTask.CodTav);
                }
            }
        }

        /// <summary>
        /// Effettua ciclo recupero richieste server
        /// </summary>
        private async Task ServerGetRequestsAsync()
        {
            // solo se NON sono disabilitati i Task2Exe...
            if (!IOBConfFull.Device.DisabExeTask)
            {
                // se non ho veto task2exe
                DateTime adesso = DateTime.Now;
                if (adesso > dtVetoTask2Exe)
                {
                    // blocco fisso a 5 sec x ora
                    dtVetoTask2Exe = adesso.AddSeconds(5);
                    // recupero elenco delle cose da fare
                    string resp = await getTask2exe("");
                    // se ho qualcosa --> creo obj e lo accodo...
                    if (!string.IsNullOrEmpty(resp) && resp.Length > 2)
                    {
                        accodaServReq("", resp);
                        if (isMulti)
                        {
                            foreach (var item in IOBConfFull.Device.MultiIobList)
                            {
                                resp = await getTask2exe(item);
                                accodaServReq(item, resp);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Effettua ciclo invio rispsote (=esito esecuzione richieste) al server
        /// </summary>
        private async Task ServerPutRespAsync()
        {
            // verifico SE la coda abbia dei valori...
            if (QueueSrvResp.Count > 0)
            {
                // invio pacchetto di dati (max da conf)
                for (int i = 0; i < nMaxSend; i++)
                {
                    // SE ho qualcosa in coda...
                    if (QueueSrvResp.Count > 0)
                    {
                        string currVal = "";
                        if (MPOnline)
                        {
                            if (IobOnline)
                            {
                                List<string> listaValori = new List<string>();
                                // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                                if (QueueSrvResp.Count > maxJsonData)
                                {
                                    // prendoi primi maxJsonDataValori
                                    for (int j = 0; j < maxJsonData; j++)
                                    {
                                        QueueSrvResp.TryDequeue(out currVal);
                                        listaValori.Add(currVal);
                                    }
                                    await SendTaskResult(listaValori);
                                }
                                else
                                {
                                    // invio in blocco
                                    listaValori = QueueSrvResp.ToList();
                                    await SendTaskResult(listaValori);
                                    // svuoto!
                                    QueueSrvResp = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueServResp", IOBConfFull.General.EnabRedisQue, redisMan);
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Imposto alcuni valori di default
        /// </summary>
        /// <param name="resetQueue">indica se sia richiesto di SVUOTARE le code delle info</param>
        private void setDefaults(bool resetQueue)
        {
            numSim = utils.CRI("numSim");
            lastPrgName = "";
            nReadIN = 0;
            nReadFilt = 0;
            nSendOut = 0;
            currMode = 0;
            lastAlarm = "";
            doStartMemDump = utils.CRB("doStartMemDump");
            doSampleMemory = utils.CRB("doSampleMemory");
            // fix url wait random...
            urlRandWait = utils.CRI("urlRandWait");
            // fix fattore demoltiplica dynData
            fixDemFactDynData();

            // svuoto code se richiesto
            if (resetQueue)
            {
                string codIob = IOBConfFull.General.FilenameIOB;
                bool useRedis = IOBConfFull.General.EnabRedisQue;
                QueueIN.Dispose();
                QueueIN = new DataQueue(codIob, "QueueIN", useRedis, redisMan);
                QueueSrvResp.Dispose();
                QueueSrvResp = new DataQueue(codIob, "QueueServResp", useRedis, redisMan);
                // no coda redis
                QueueAlarm = new DataQueue(codIob, "QueueAlarm", false, redisMan);
                QueueFLog = new DataQueue(codIob, "QueueFLog", false, 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);
            }
            // imposto contatori blink a zero...
            i_counters = new int[32];
            lastPeriodicLog = DateTime.Now;

            // fix parametri generali...
            pzCountDelay = IOBConfFull.Device.PzCountDelay;
            enablePrgName = IOBConfFull.Device.EnabProgName;
            enablePzCountByApp = IOBConfFull.Device.EnabPzCount;
            mem2trace = IOBConfFull.Special.BankConf != null ? IOBConfFull.Special.BankConf.Mem2Trace : "";
            numArtCharTrim = IOBConfFull.Device.NumArtCharTrim;

            // valore standard divieto accodamento segnali IN
            vetoQueueIn = IOBConfFull.Device.StartupVetoQueueIN;
            // fix slow data
            enableSlowData = IOBConfFull.FluxLog.EnableSlowData;

            ENABLE_MEM_REWRITE = IOBConfFull.Device.EnabMemRewrite;
            GLOBAL_DBAND = IOBConfFull.FluxLog.GlobalDeadBand;
            enabSendMachineConf = IOBConfFull.General.SenMachineConf;
            resetAlarmOnStart = IOBConfFull.General.ResetAlarmOnStart;
            disableOdl = IOBConfFull.Odl.DisableOdl;
        }

        private async Task SvuotaCodaContapezziAsync()
        {
            // verifico non sia impedita la gestione contapezzi (per evitare invio su macchine lente nel reset all'attrezzaggio)
            if (DateTime.Now <= vetoQueuePzCount)
            {
                lgInfo($"Ciclo SvuotaCodaContapezziAsync DISABILITATO fino a {vetoQueuePzCount}");
            }
            else
            {
                // permetto al max 2 tentativi infruttuosi...
                int maxTry = 2;
                int oldContapezzi = contapezziIOB;
                // SE HO gestione contapezzi...
                if (!IOBConfFull.Device.EnabPzCount)
                {
                    lgDebug($"Contapezzi disabilitato da configuraizone: IOBConfFull.Device.EnabPzCount: {IOBConfFull.Device.EnabPzCount}");
                }
                else
                {
                    // se ho contapezzi OLTRE limite...
                    while ((MPOnline) && (contapezziPLC > contapezziIOB + minSendPzCountBlock))
                    {
                        lgInfo($"Ciclo SvuotaCodaContapezziAsync --> contapezziPLC: {contapezziPLC} | contapezziIOB: {contapezziIOB}");
                        if (!isMulti)
                        {
                            pzCntReload(true);
                        }
                        // provo invio
                        if (!isMulti)
                        {
                            trySendPzCountBlock();
                        }
                        // verifica per evitare loop infinito invio fallito
                        if (oldContapezzi == contapezziIOB)
                        {
                            maxTry--;
                        }
                        else
                        {
                            maxTry = 2;
                            oldContapezzi = contapezziIOB;
                        }
                        // verifico maxTry: se li ho esauriti esco!
                        if (maxTry <= 0)
                        {
                            return;
                        }
                        // aspetto x dare tempo calcolo
                        await Task.Delay(500);
                    }
                }
            }
        }

        /// <summary>
        /// Processo la coda FLog...
        /// </summary>
        private async Task SvuotaCodaFLogAsync()
        {
            bool sendOnMachineOff = false;
            // controllo se è passato oltre watchdog e non ho inviato nulla --> RE-INVIO (ultimo inviato)!!!!
            if (DateTime.Now.Subtract(lastWatchDog).TotalSeconds > IOBConfFull.General.WatchDogSec)
            {
                string wdStatus = "elapsed";
                string sVal = string.Format("[WDST]{0}", wdStatus);
                // chiamo accodamento... SE non disabilitato..
                if (!disableWdst)
                {
                    bool sent = accodaFLog("WDST", sVal, qEncodeFLog("WDST", wdStatus));
                    if (sent)
                    {
                        // traccio valore DynData x analisi
                        trackDynData("WDST", wdStatus);
                    }
                }
                lastWatchDog = DateTime.Now;
                sendOnMachineOff = true;
                lgInfo("Impostato sendOnMachineOff a true");
            }
            // verifico SE la coda abbia dei valori...
            if (QueueFLog.Count > 0)
            {
                // invio pacchetto di dati (max da conf)
                for (int i = 0; i < nMaxSend; i++)
                {
                    // SE ho qualcosa in coda...
                    if (QueueFLog.Count > 0)
                    {
                        string currVal = "";
                        if (MPOnline)
                        {
                            if (IobOnline || sendOnMachineOff)
                            {
                                // se ho + di 2 elementi in coda --> uso invio JSON in blocco...
                                if (QueueFLog.Count > 1)
                                {
                                    List<string> listaValori = new List<string>();
                                    // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                                    if (QueueFLog.Count > maxJsonData)
                                    {
                                        // prendoi primi maxJsonDataValori
                                        for (int j = 0; j < maxJsonData; j++)
                                        {
                                            QueueFLog.TryDequeue(out currVal);
                                            listaValori.Add(currVal);
                                        }
                                        await sendDataBlock(urlType.FLog, listaValori);
                                        lastWatchDog = DateTime.Now;
                                    }
                                    else
                                    {
                                        // invio in blocco
                                        listaValori = QueueFLog.ToList();
                                        // invio
                                        await sendDataBlock(urlType.FLog, listaValori);
                                        // svuoto! NO redis
                                        QueueFLog = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueFLog", false, redisMan);
                                        //QueueFLog = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueFLog", IOBConfFull.General.EnabRedisQue, redisMan);
                                        lastWatchDog = DateTime.Now;
                                    }
                                }
                                else
                                {
                                    // INVIO SINGOLO...!!!
                                    QueueFLog.TryDequeue(out currVal);
                                    await sendToMoonPro(urlType.FLog, currVal);
                                    lastWatchDog = DateTime.Now;
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Processo la coda RawTransf...
        /// </summary>
        private async Task<bool> SvuotaCodaRawTransfAsync(bool force = false)
        {
            bool fatto = false;
            // verifico SE la coda abbia dei valori...
            if (QueueRawTransf.Count > 0)
            {
                // invio pacchetto di dati (max da conf)
                for (int i = 0; i < nMaxSend; i++)
                {
                    // SE ho qualcosa in coda...
                    if (QueueRawTransf.Count > 0)
                    {
                        string currVal = "";
                        if (MPOnline || force)
                        {
                            if (IobOnline || force)
                            {
                                List<string> listaValori = new List<string>();
                                // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                                if (QueueRawTransf.Count > maxJsonData)
                                {
                                    // prendoi primi maxJsonDataValori
                                    for (int j = 0; j < maxJsonData; j++)
                                    {
                                        QueueRawTransf.TryDequeue(out currVal);
                                        listaValori.Add(currVal);
                                    }
                                    fatto = await sendDataBlock(urlType.RawTransf, listaValori, force);
                                }
                                else
                                {
                                    // invio in blocco
                                    listaValori = QueueRawTransf.ToList();
                                    // invio
                                    fatto = await sendDataBlock(urlType.RawTransf, listaValori, force);
                                    if (fatto)
                                    {
                                        // svuoto se ha okReport!
                                        QueueRawTransf = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueRawTransf", false, redisMan);
                                    }
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
            return fatto;
        }

        /// <summary>
        /// Processo la coda UserLog...
        /// </summary>
        private async Task SvuotaCodaULogAsync()
        {
            // verifico SE la coda abbia dei valori...
            if (QueueULog.Count > 0)
            {
                // invio pacchetto di dati (max da conf)
                for (int i = 0; i < nMaxSend; i++)
                {
                    // SE ho qualcosa in coda...
                    if (QueueULog.Count > 0)
                    {
                        string currVal = "";
                        if (MPOnline)
                        {
                            if (IobOnline)
                            {
                                List<string> listaValori = new List<string>();
                                // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                                if (QueueULog.Count > maxJsonData)
                                {
                                    // prendoi primi maxJsonDataValori
                                    for (int j = 0; j < maxJsonData; j++)
                                    {
                                        QueueULog.TryDequeue(out currVal);
                                        listaValori.Add(currVal);
                                    }
                                    await sendDataBlock(urlType.ULog, listaValori);
                                }
                                else
                                {
                                    // invio in blocco
                                    listaValori = QueueULog.ToList();
                                    // invio
                                    await sendDataBlock(urlType.ULog, listaValori);
                                    // svuoto!
                                    QueueULog = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueULog", false, redisMan);
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

        private void UpdateIobState(bool newStatus)
        {
            // Log se lo stato è cambiato
            DateTime adesso = DateTime.Now;
            if (IobOnline != newStatus || adesso.Subtract(lastIobStatusDisplUpdate).TotalMilliseconds > (baseUtils.nextPauseSendMSec * 10))
            {
                lgInfo(newStatus ? "IOB ONLINE for server MP/IO" : "IOB OFFLINE for server MP/IO");
                IobOnline = newStatus;
                lastIobStatusDisplUpdate = adesso;
                if (newStatus)
                {
                    lastIobOnline = adesso;
                    parentForm.commSrvActive = 2; // Stato Online
                }
                else
                {
                    parentForm.commSrvActive = 1; // Stato Degradato/Offline
                }
            }

            // Imposto il veto per il prossimo controllo (se ConnOk + rapido sennò meno rapido)
            var msWait = baseUtils.nextPauseSendMSec * (connectionOk ? 12 : 150);
            dtVetoCheckIOB = adesso.AddMilliseconds(msWait);
        }

        #endregion Private Methods
    }
}