using EgwProxy.Ftp;
using IOB_UT_NEXT;
using IOB_UT_NEXT.Config;
using MapoSDK;
using MathNet.Numerics.Statistics;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using NLog.Targets.Wrappers;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Serialization;
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 Public Fields

        public int numPzReqOdl = 0;

        #endregion Public Fields

        #region Public Constructors

        /// <summary>
        /// inizializzo l'oggetto sulla form SULLA BASE DEL FILE DI CONFIGURAZIONE letto
        /// </summary>
        /// <param name="caller">Form chiamante</param>
        /// <param name="IobConfNew">Configurazione (v 4.x)</param>
        public Generic(AdapterForm caller, IobConfTree IobConfNew)
        {
            // salvo il form chiamante
            parentForm = caller;
            if (IobConfNew != null)
            {
                // salvo configurazione...
                IOBConfFull = IobConfNew;

                // init oggetto redis...
                redisMan = new RedisIobCache(IobConfNew.MapoMes.IpAddr, IobConfNew.General.FilenameIOB, $"{IobConfNew.General.IobType}", IobConfNew.General.MinDeltaSec);

                // init code
                SetupQueue();

                // initi oggetto TCMan
                tcMan = new TCMan(IobConfNew.TCDataConf.Lambda, IobConfNew.TCDataConf.MaxDelayFactor, IobConfNew.TCDataConf.MaxIncrPz);

                lastConnectTry = DateTime.Now;

                lgInfo("Avvio preliminare AdapterGeneric");
                lastLogStartup = DateTime.Now;

                // setup currProdData & last prod data
                currProdData = redisMan.redGetHashDict(rKeyCurrProdData);
                // i last li avvio a VUOTI... x evitare errore mancata riscrittura SIMEC che ha memorie NON ritentive
                lastProdData = new Dictionary<string, string>();
                //lastProdData = new Dictionary<string, string>(currProdData);
                // aggiungo altri defaults
                setDefaults(true);
                // imposta valori memoria (e resetta parametri su server)
                setParamPlc();

                // checkLogDir x shrink!
                checkShrinkDir();

                // imposto veto invio per i prossimi sec
                dtVetoQueueIN = DateTime.Now.AddSeconds(vetoQueueIn);
                string msgVeto = $"Impostato veto QUEUE-IN a {vetoQueueIn}s | fino alle {dtVetoQueueIN:yyyy.MM.dd HH:mm:ss}";
                lgInfoStartup(msgVeto);
                lgInfo(msgVeto);

                // invio info IOB
                SendM2IOB();
                // invio altri dati accessori...
                SendMachineConf();
                if (resetAlarmOnStart)
                {
                    SendAlarmReset();
                }
                // concluso!
                lgInfoStartup("Istanziata classe preliminare IOBGeneric");
            }
            else
            {
                lgError("Error: IobCOnf is null!");
            }
        }

        #endregion Public Constructors

        #region Public Events

        /// <summary>
        /// Evento Iob ha subito un refresh
        /// </summary>
        public event EventHandler<iobRefreshedEventArgs> eh_refreshed;

        #endregion Public Events

        #region Public Properties

        /// <summary>
        /// Salva verifica stato connessione OK con macchina (PLC/CNC)
        /// </summary>
        /// <returns></returns>
        public virtual bool connectionOk
        {
            get
            {
                return _connOk || DemoIn;
            }
            set
            {
                _connOk = value;
            }
        }

        /// <summary>
        /// Contapezzi attuale
        /// </summary>
        public Int32 contapezziIOB
        {
            get
            {
                return tcMan.pzCountIOB;
            }
            set
            {
                tcMan.pzCountIOB = value;
            }
        }

        /// <summary>
        /// Ultima lettura variabile contapezzi da CNC
        /// </summary>
        public Int32 contapezziPLC
        {
            get
            {
                return tcMan.pzCountPLC;
            }
            set
            {
                tcMan.pzCountPLC = value;
            }
        }

        /// <summary>
        /// Contatore x invio dati FluxLog
        /// </summary>
        public int counterFLog { get; set; }

        /// <summary>
        /// Contatore x invio dati RawTransf
        /// </summary>
        public int counterRawTransf { get; set; }

        /// <summary>
        /// Contatore x invio dati SignalIN
        /// </summary>
        public int counterSigIN { get; set; }

        /// <summary>
        /// Contatore x invio dati UserLog
        /// </summary>
        public int counterULog { get; set; }

        /// <summary>
        /// nome Programma corrente
        /// </summary>
        public string currPrgName { get; set; }

        /// <summary>
        /// Verifica se sia in modalità DEMO --&gt; da tipo IOB SIMULA...
        /// </summary>
        public bool DemoIn
        {
            get => IOBConfFull.General.IobType == tipoAdapter.SIMULA;
        }

        /// <summary>
        /// Dizionario contapezzi Macchina (valori da impianto) x macchine multi tavola/pallet
        /// </summary>
        public Dictionary<string, int> DictPzCountImp { get; set; } = new Dictionary<string, int>();

        /// <summary>
        /// Dizionario contapezzi MES (valori salvati su server) x macchine multi tavola/pallet
        /// </summary>
        public Dictionary<string, int> DictPzCountMes { get; set; } = new Dictionary<string, int>();

        /// <summary>
        /// Indica se la chiamata WDST dit racking watchdog sia disabilitata dall'invio nel FluxLog
        /// </summary>
        public bool disableWdst { get; set; } = false;

        /// <summary>
        /// Indica lo stato Online/Offline della IOB
        /// </summary>
        public bool IobOnline
        {
            get
            {
                return utils.IOB_Online;
            }
            set
            {
                utils.IOB_Online = value;
            }
        }

        /// <summary>
        /// Verifica se sia macchina multi = DoppioPallet da CONF
        /// </summary>
        public bool isMulti
        {
            get => IOBConfFull.Device.IsMulti;
        }

        /// <summary>
        /// Log verboso da configurazione (SOLO CHIAVE "verbose"...)
        /// </summary>
        public bool isVerboseLog { get; set; } = utils.CRB("verbose");

        /// <summary>
        /// Ultimo Alarm letto
        /// </summary>
        public string lastAlarm { get; set; }

        /// <summary>
        /// Ultimo ARRAY DynData letto
        /// </summary>
        public Dictionary<string, string> lastDynData { get; set; } = new Dictionary<string, string>();

        /// <summary>
        /// Ultimo DynData (sunto) letto
        /// </summary>
        public string lastDynDataCtrlVal { get; set; }

        /// <summary>
        /// Ultimo Override set letto
        /// </summary>
        public string lastOverrideFS { get; set; }

        /// <summary>
        /// Ultimo Override set letto
        /// </summary>
        public string lastOverrideRapid { get; set; }

        /// <summary>
        /// Ultimo programma letto
        /// </summary>
        public string lastPrgName { get; set; }

        /// <summary>
        /// Ultimo SysInfo letto
        /// </summary>
        public string lastSysInfo { get; set; }

        /// <summary>
        /// Ultimo URL
        /// </summary>
        public string lastUrl { get; set; }

        /// <summary>
        /// Valore massimo accettato x incremento pezzi letti dal PLC (valore moltiplicato per
        /// 100... 200% --&gt; 200)
        /// </summary>
        public int maxPzDeltaPerc
        {
            get => IOBConfFull.Counters.MaxIncrPzCountPerc;
        }

        /// <summary>
        /// Verifica SE si debba fare log periodico (ogni "verboseLogTOut" sec...)
        /// </summary>
        public bool periodicLog
        {
            get
            {
                bool answ = false;
                answ = (DateTime.Now.Subtract(lastPeriodicLog).TotalSeconds > utils.CRI("verboseLogTOut"));
                if (answ)
                {
                    lastPeriodicLog = DateTime.Now;
                }

                return answ;
            }
        }

        /// <summary>
        /// Valore medio del TC rilevato x verifica derive sul delta variazione contapezzi
        /// </summary>
        public double plcAvgTc
        {
            get
            {
                double answ = tcMan.avgTC > 0 ? tcMan.avgTC : 1;
                return answ;
            }
        }

        /// <summary>
        /// DataOra dell'ultima lettura variabile contapezzi da CNC
        /// </summary>
        public DateTime plcLastPzRead
        {
            get
            {
                return tcMan.lastObservedData;
            }
        }

        /// <summary>
        /// Determina se il contapezzi plc sia valido (lo è se data avvio adapter è prima di ultimo dato registrato in contapezzi)
        /// </summary>
        public bool plcPzCountValid
        {
            get => tcMan.lastObservedData > dtAvvioAdp;
        }

        /// <summary>
        /// Abilitazione coda segnali ingresso
        /// </summary>
        public bool queueInEnabCurr
        {
            get => qInEnabCurr;
            set
            {
                qInEnabCurr = value;
                lgInfo($"SET queueInEnabCurr: {value} | {DateTime.Now:HHmmss}");
            }
        }

        /// <summary>
        /// Finestra dei byte da mostrare di default x il RawDataInput
        /// </summary>
        public int RawDataInputSize { get; set; } = 8;

        /// <summary>
        /// Indice di partenza della memoria RawData Input da mostrare
        /// </summary>
        public int RawDataInputStart { get; set; } = 0;

        /// <summary>
        /// URL per segnalazione reboot...
        /// </summary>
        public string urlReboot
        {
            get => $@"{urlCommandIobFile("sendReboot")}?mac={GetMACAddress()}";
        }

        /// <summary>
        /// URL per salvataggio dati conf YAML completi IOB...
        /// </summary>
        public string urlSaveConfYaml
        {
            get => $@"{urlCommandIobFile("saveConfYaml")}";
        }

        /// <summary>
        /// Verifica SE si debba fare log verboso (verboso + ogni tot letture IN)
        /// </summary>
        public bool verboseLog
        {
            get
            {
                bool answ = false;
                int logEvery = utils.CRI("logEvery");
                if (logEvery < 1)
                {
                    logEvery = 10;
                }

                answ = utils.CRB("verbose") && (nReadIN % logEvery == 0);
                return answ;
            }
        }

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// Esegue conversione in un dizionario di tipo string/string serializzando e deserializzando
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static Dictionary<string, string> ConvertToStringDict(Dictionary<string, object> input)
        {
            return input.ToDictionary(pair => pair.Key, pair => pair.Value?.ToString());
        }

        /// <summary>
        /// Accumula in coda i valori ALARM e logga...
        /// </summary>
        /// <param name="val">VALORE RAW (x display)</param>
        /// <param name="encodedVal">VALORE già processato con qEncodeFLog(...)</param>
        public void accodaAlarmLog(string val, string encodedVal)
        {
            // mostro dati variati letti...
            displayOtherData(val);
            // accodo IN PRIMIS al FluxLog --> accodo (valore già formattato)!
            QueueFLog.Enqueue(encodedVal);
            // accodo ANCHE alla coda allarmi che trasmetterò SOLO SE a scadenza impostata (10 sec?)
            // ho allarmi perdurati...

            // loggo!
            lgInfo(string.Format("[QUEUE-ALARM-LOG] {0}", encodedVal));
            counterFLog++;
            if (counterFLog > 9999)
            {
                counterFLog = 0;
            }
        }

        /// <summary>
        /// Accumula in coda i valori Flux Log e logga. Restituisce true se inviato (x track su REDIS)
        /// </summary>
        /// <param name="codFlux">Nome del flusso da inviare (per verifica veto invio)</param>
        /// <param name="val">VALORE RAW (x display)</param>
        /// <param name="encodedVal">VALORE già processato con qEncodeFLog(...)</param>
        public bool accodaFLog(string codFlux, string val, string encodedVal)
        {
            bool enabled = false;
            // verifico se il parametro sia abilitato...
            if (IOBConfFull.Memory != null && IOBConfFull.Memory.mMapRead != null && IOBConfFull.Memory.mMapRead.ContainsKey(codFlux))
            {
                enabled = IOBConfFull.Memory.mMapRead[codFlux].sendEnabled;
                if (!enabled)
                {
                    lgDebug($"accodaFLog : invio bloccato per conf parametro sendEnabled | codFlux: {codFlux}");
                }
                else
                {
                    // processo SOLO SE ho dei valori non nulli x encodedVal...
                    if (!string.IsNullOrEmpty(encodedVal))
                    {
                        // mostro dati variati letti...
                        displayOtherData(val);
                        // --> accodo (valore già formattato)!
                        QueueFLog.Enqueue(encodedVal);
                        // se abilitato controllo coda FLog (superiore a 0...)
                        if (maxQueueFLog > 0)
                        {
                            // se ho una coda superiore a max ammesso
                            if (QueueFLog.Count > maxQueueFLog)
                            {
                                // elimino valori iniziali fino a tornare al max ammesso...
                                while (QueueFLog.Count > maxQueueFLog)
                                {
                                    string currVal = "";
                                    QueueFLog.TryDequeue(out currVal);
                                    lgInfo($"Eliminazione da coda FLog per superamento maxLengh: {currVal}");
                                }
                            }
                        }
                        // loggo!
                        lgTrace($"[QUEUE-FLOG] {encodedVal}");
                        counterFLog++;
                        if (counterFLog > 9999)
                        {
                            counterFLog = 0;
                        }
                    }
                    else
                    {
                        lgTrace($"ERRORE in [QUEUE-FLOG] : encodedVal vuoto | val: {val}");
                    }
                }
            }
            else
            {
                // registro nel dizionario dei valori FluxLog filtrati
                SaveFiltFluxLog(codFlux);
                // verifico se registrare elenco valori filtrati in log...
                FiltFluxLogCheckSave(100);
            }
            return enabled;
        }

        /// <summary>
        /// Accoda (visualizzando in cima allo stack) la nuova stringa di output per area OTHER DATA
        /// </summary>
        /// <param name="newLine"></param>
        public void accodaOtherData(string newLine)
        {
            // inserisco in cima allo stack
            parentForm.WriteTextSafe(newLine);
        }

        /// <summary>
        /// Accumula in coda i valori RawData + log
        /// </summary>
        /// <param name="mesType"></param>
        /// <param name="mesContent"></param>
        public void accodaRawData(rawTransfType mesType, object mesContent)
        {
            /*--------------------------------
             * nuova gestione coda dictionary
             * fixme todo da fare !!!
             *
             * - conterrà una lista di oggetti baseRawTransf
             * - i dati vanno poi "scodati" dal + vecchio ed inviati a MP/IO
             * - mostra un sunto delle info da inviare
             * - accodamento vero e proprio
             * - verifica (opzionale) coda massima x gestire roundRobin ultimi eventi
             * - trace della coda
             * - counter invio??? valutare se c'è dataora e poi sono da salvare su MongoDb / Redis
             *
             * */

            // serializzo il valore...
            JObject njObj;
            if (mesType == rawTransfType.IcoelBatch || mesType == rawTransfType.IcoelVarInfo)
            {
                njObj = (JObject)mesContent;
            }
            else
            {
                njObj = (JObject)JToken.FromObject(mesContent);
            }
            BaseRawTransf newVal = new BaseRawTransf(DateTime.Now, njObj, mesType);

            string encodedVal = JsonConvert.SerializeObject(newVal);
            // --> accodo (valore già formattato)!
            QueueRawTransf.Enqueue(encodedVal);
            // se abilitato controllo coda Max (superiore a 0...)
            if (maxQueueRawTransf > 0)
            {
                // se ho una coda superiore a max ammesso
                if (QueueRawTransf.Count > maxQueueRawTransf)
                {
                    // elimino valori iniziali fino a tornare al max ammesso...
                    while (QueueRawTransf.Count > maxQueueRawTransf)
                    {
                        string currVal = "";
                        QueueRawTransf.TryDequeue(out currVal);
                        lgInfo($"Eliminazione da coda RawTransf per superamento maxLengh: {currVal}");
                    }
                }
            }
            // loggo!
            lgTrace(string.Format("[QUEUE-RTRANSF] {0}", encodedVal));
            counterRawTransf++;
            if (counterRawTransf > 9999)
            {
                counterRawTransf = 0;
            }
        }

        /// <summary>
        /// Accumula in coda i valori Signal IN e logga...
        /// <paramref name="currDispData">Parametri da aggiornare x display in form</paramref>
        /// </summary>
        public void accodaSigIN(ref newDisplayData currDispData)
        {
            DateTime adesso = DateTime.Now;
            lgDebug($"accodaSigIN 01 | qEncodeIN: {qEncodeIN}");
            // mostro dati variati letti...
            displayInData(ref currDispData);
            // verifico veto a invio status macchina
            if (IOBConfFull.Device.DisabSigIn)
            {
                lgTrace($"Filtrato accodamento valore da conf IOB | DisabSigIn | [QUEUE-IN] {qEncodeIN}");
            }
            else
            {
                // verifico non sia in veto invio iniziale...
                if (queueInEnabCurr)
                {
                    // --> accodo (valore già formattato)!
                    QueueIN.Enqueue(qEncodeIN);
                    // loggo!
                    lgDebug($"[QUEUE-IN] {qEncodeIN}");
                }
                else
                {
                    lgTrace($"[VETO FOR QUEUE-IN] | {qEncodeIN} - MESSAGE NOT SENT | {adesso:yyyyMMdd_HHmmss}");
                    checkVetoQueueIn();
                }
                // aggiorno counters ed eventuale reset
                nReadFilt++;
                if (nReadFilt > int.MaxValue - 1)
                {
                    nReadFilt = 0; // per evitare buffer overflow...
                }

                counterSigIN++;
                if (counterSigIN > 9999)
                {
                    counterSigIN = 0;
                }
            }
            lgDebug($"accodaSigIN 02 | nReadFilt: {nReadFilt} | counterSigIN: {counterSigIN} | QueueIN len: {QueueIN.Count}");
        }

        /// <summary>
        /// Accumula in coda i valori USER LOG e logga...
        /// </summary>
        /// <param name="val">VALORE RAW (x display)</param>
        /// <param name="encodedVal">VALORE già processato con qEncodeULog(...)</param>
        public void accodaUserLog(string val, string encodedVal)
        {
            // mostro dati variati letti...
            displayOtherData(val);
            // accodo IN PRIMIS al FluxLog --> accodo (valore già formattato)!
            QueueULog.Enqueue(encodedVal);

            // loggo!
            lgInfo(string.Format("[QUEUE-USER-LOG] {0}", encodedVal));
            counterULog++;
            if (counterULog > 9999)
            {
                counterFLog = 0;
            }
        }

        /// <summary>
        /// Verifica se la IOB sia ENABLED (da server o Demo)
        /// </summary>
        public async Task<bool> CheckIobEnabled()
        {
            // 1. Controllo Veto (Sincrono)
            if (dtVetoCheckIOB >= DateTime.Now)
                return IobOnline;

            bool currentAnsw = false;

            if (DemoOut)
            {
                currentAnsw = (QueueIN.Count + QueueFLog.Count >= nMaxSend);
            }
            else
            {
                currentAnsw = await ExecuteIobCheckWithRetry(maxRetries: 5);
            }

            // 3. Gestione Stato e Logica UI
            UpdateIobState(currentAnsw);

            return currentAnsw;
        }

        /// <summary>
        /// Verifica veto invio per una chiave specifica in Redis
        /// </summary>
        /// <param name="keyReq"></param>
        /// <returns></returns>
        public bool CheckSendVeto(string keyReq, TimeSpan vetoReq)
        {
            DateTime adesso = DateTime.Now;
            DateTime lastSend = LastSendGet(keyReq);
            bool sendEnab = lastSend.Add(vetoReq) <= adesso;
            return sendEnab;
        }

        /// <summary>
        /// Verifica se il server sia ALIVE (tramite PING)
        /// </summary>
        public bool CheckServerAlive() // Rimosso async, restituisce bool
        {
            // 1. Controllo Veto (Sincrono)
            if (dtVetoPing >= DateTime.Now) return MPOnline;
            if (DemoOut) return true;

            // 2. Eseguo la parte asincrona forzando l'attesa
            // Usiamo Task.Run per far girare il codice asincrono su un thread del pool
            // evitando deadlock con il thread della UI
            bool isAlive = Task.Run(async () => await ExecuteApiCheckWithRetryAsync(maxRetries: 7))
                               .GetAwaiter()
                               .GetResult();

            // 3. Gestione Stato (Sincrono)
            UpdateServerState(isAlive);

            return isAlive;
        }

        /// <summary>
        /// Verifica se il server sia ALIVE (tramite PING)
        /// </summary>
        public async Task<bool> CheckServerAliveAsync()
        {
            // 1. Controllo Veto (Sincrono)
            if (dtVetoPing >= DateTime.Now) return MPOnline;
            if (DemoOut) return true;

            bool isAlive = await ExecuteApiCheckWithRetryAsync(maxRetries: 7);

            // 3. Gestione Stato (Sincrono)
            UpdateServerState(isAlive);

            return isAlive;
        }

        /// <summary>
        /// Verifica veto coda QueueIN ed aggiorna abilitazione su variabile
        /// </summary>
        public void checkVetoQueueIn()
        {
            queueInEnabCurr = dtVetoQueueIN < DateTime.Now;
        }

        /// <summary>
        /// Update visualizzaizone BIT in ingresso <paramref name="currDispData">Parametri da
        /// aggiornare x display in form</paramref>
        /// </summary>
        public void displayInData(ref newDisplayData currDispData)
        {
            if (currDispData != null)
            {
                // mostro update...
                string newString = string.Format("{0:0000}|{1}", counterSigIN, utils.IntToBinStr(B_output, 8));
                currDispData.newInData = $"{B_output:X}";
                currDispData.newSignalData = newString;
            }
        }

        /// <summary>
        /// Mostra cosa ha/avrebbe inviato
        /// </summary>
        /// <param name="newData"></param>
        public void displayOtherData(string newData)
        {
            // mostro update...
            accodaOtherData(newData);
        }

        /// <summary>
        /// Effettua i task di comunicazione IN/OUT con la macchina
        /// </summary>
        /// <param name="ciclo"></param>
        /// <returns></returns>
        public void doMachineTask(gatherCycle ciclo)
        {
            // init obj display
            newDisplayData currDispData = new newDisplayData();
            // controllo connessione/connettività
            if (connectionOk)
            {
                // controllo non sia già in esecuzione...
                if (!adpCommAct)
                {
                    // provo ad avviare
                    try
                    {
                        // imposto flag adapter running..
                        adpCommAct = true;
                        adpStartRun = DateTime.Now;
                    }
                    catch (Exception exc)
                    {
                        string errore = $"Adapter NOT STARTED!!!{Environment.NewLine}{exc}";
                        adpCommAct = false;
                        adpStartRun = DateTime.Now;
                        currDispData.newLiveLogData = errore;
                    }
                    if (adpCommAct)
                    {
                        // try / catch generale altrimenti segno che è disconnesso...
                        try
                        {
                            if (ciclo == gatherCycle.VHF)
                            {
                                //processVHF();
                            }
                            // processing dati memoria (lettura, filtraggio, enqueque)
                            else if (ciclo == gatherCycle.HF)
                            {
                                processWhatchDog();
                                Thread.Sleep(5);
                                processAllMemory();
                            }
                            else if (ciclo == gatherCycle.MF)
                            {
                                processMode();
                                Thread.Sleep(5);
                                ExecServerRequests();
                                Thread.Sleep(5);
                                processCustomTaskMF();
                                Thread.Sleep(5);
                                processOverride();
                                Thread.Sleep(5);
                                processContapezzi();
                                Thread.Sleep(5);
                                processCncAlarms();
                                Thread.Sleep(5);
                                processDynData();
                                Thread.Sleep(5);
                                processMem2Write();
                            }
                            else if (ciclo == gatherCycle.LF)
                            {
                                processCustomTaskLF();
                                Thread.Sleep(5);
                                processOtherCounters().GetAwaiter().GetResult(); ;
                                Thread.Sleep(5);
                                processProgram();
                            }
                            else if (ciclo == gatherCycle.VLF)
                            {
                                //processRecipeSyncArch();
                                // recupero dati SETUP (sysinfo) e li invio/mostro se variati...
                                processSysInfo();
                                if (enableSlowData)
                                {
                                    Thread.Sleep(5);
                                    processSlowDataRead();
                                }
                            }

                        }
                        catch (Exception exc)
                        {
                            // segnalo eccezione e indico disconnesso...
                            lgError($"Exception doMachineTask | {ciclo} | fermo adapter{Environment.NewLine}{exc}");
                            parentForm.fermaAdapter(true, false, true);
                        }
                        // tolgo flag running
                        adpCommAct = false;
                    }
                    else
                    {
                        if (periodicLog)
                        {
                            lgDebug("ADP not running...");
                        }
                    }
                }
                else
                {
                    // log ADP running
                    lgInfo("Non eseguo chiamata: ADP ancora in running");
                    // se è bloccato da oltre maxSec lo sblocco...
                    if (DateTime.Now.Subtract(adpStartRun).TotalSeconds > utils.CRI("maxAdapterLockSec"))
                    {
                        // tolgo flag running
                        adpCommAct = false;
                        adpStartRun = DateTime.Now;
                    }
                }
            }
            else
            {
                //// anche se NON connesso alcuni task di bassa freq li eseguo...
                //if (ciclo == gatherCycle.LF)
                //{
                //    // verifico se devo gestire cambio ODL in modo automatico
                //    await ProcessAutoOdlAsync();
                //    // verifico se devo gestire auto generazione dossier quotidiana
                //    ProcessAutoDossier();
                //    // effettua gestione import file se configurato...
                //    await ProcessFileImportAsync();
                //    // effettua process ritorno ricette
                //    await ProcessRecipeFileRetAsync();
                //}
                //else if (ciclo == gatherCycle.VLF)
                //{
                //    processRecipeSyncArch();
                //}

                // provo a riconnettere SE abilitato tryRestart...
                if (adpTryRestart && !connectionOk)
                {
                    // controllo se sia scaduto periodi di veto al tryConnect...
                    int waitRecMSec = utils.CRI("waitRecMSec");
                    // cerco se ci sia un valore in ovverride x il singolo IOB...
                    if (IOBConfFull.General.WaitRecMsec > 0)
                    {
                        waitRecMSec = IOBConfFull.General.WaitRecMsec;
                    }
                    DateTime dtVeto = lastConnectTry.AddMilliseconds(waitRecMSec);
                    if (DateTime.Now > dtVeto)
                    {
                        lgInfo($"Veto Time Elapsed | lastConnectTry: {lastConnectTry} | waitRecMSec {waitRecMSec} ms) | NOW tryConnect");
                        lastConnectTry = DateTime.Now;
                        tryConnect();
                    }
                }
                currDispData.semIn = Semaforo.SR;
                processDisconnectedTask();
                processMemoryDiscon();
            }
            raiseRefresh(currDispData);
        }

        /// <summary>
        /// Effettua i task di comunicazione IN/OUT con il server MAPO
        /// </summary>
        /// <param name="ciclo"></param>
        /// <returns></returns>
        public async Task doServerTaskAsync(gatherCycle ciclo)
        {
            // init obj display
            newDisplayData currDispData = new newDisplayData();
            // IN OGNI CASO a prima di tutto EFFETTUO GESTIONE INVII dati da code!!!
            try
            {
                await TrySendValuesAsync();
            }
            catch (Exception exc)
            {
                lgError(exc, "Errore in gestione svuotamento/invio preliminare code memoria");
                currDispData.semOut = Semaforo.SR;
            }
            // controllo connessione/connettività verso PLC...
            if (connectionOk)
            {
                // try / catch generale altrimenti segno che è disconnesso...
                try
                {
                    bool showDebugData = false;
                    if (ciclo == gatherCycle.VHF)
                    {
                        processVHF();
                    }
                    // processing dati memoria (lettura, filtraggio, enqueque)
                    else if (ciclo == gatherCycle.HF)
                    {
                        // recupera ed invia risposte al server
                        await ServerPutRespAsync();
                    }
                    else if (ciclo == gatherCycle.MF)
                    {
                        // recupero elenco richieste
                        await ServerGetRequestsAsync();
                    }
                    else if (ciclo == gatherCycle.LF)
                    {

                        // verifico se devo gestire cambio ODL in modo automatico
                        await ProcessAutoOdlAsync();
                        // verifico se devo gestire auto generazione dossier quotidiana
                        ProcessAutoDossier();
                        // effettua gestione import file se configurato...
                        await ProcessFileImportAsync();
                        // effettua process ritorno ricette
                        await ProcessRecipeFileRetAsync();
                    }
                    else if (ciclo == gatherCycle.VLF)
                    {
                        if (utils.CRB("enableContapezzi"))
                        {
                            // rilettura contapezzi da server...
                            lgTrace("Ciclo MsVLF: pzCntReload(true)");
                            if (!isMulti)
                            {
                                pzCntReload(true);
                            }

                            // refresh associazione Macchina - IOB
                            await SendM2IobAsync();
                            // invio altri dati accessori...
                            await SendMachineConfAsync();
                        }
                        // checkLogDir x shrink!
                        checkShrinkDir();
                        // eventuale log!
                        if (utils.CRB("recTime"))
                        {
                            try
                            {
                                logTimeResults();
                            }
                            catch
                            { }
                        }
                        processRecipeSyncArch();
                    }

                    // mostra eventuali altri dati di processo...
                    reportDataProc();
                    if (showDebugData)
                    {
                        // verifica se debba salvare e mostrare dati
                        checkSavePersDataLayer();
                    }
                }
                catch (Exception exc)
                {
                    // segnalo eccezione e indico disconnesso...
                    lgError($"Exception | doServerTaskAsync | {ciclo}{Environment.NewLine}{exc}");
                }
            }
            else
            {
                // anche se NON connesso alcuni task di bassa freq li eseguo...
                if (ciclo == gatherCycle.LF)
                {
                    // verifico se devo gestire cambio ODL in modo automatico
                    await ProcessAutoOdlAsync();
                    // verifico se devo gestire auto generazione dossier quotidiana
                    ProcessAutoDossier();
                    // effettua gestione import file se configurato...
                    await ProcessFileImportAsync();
                    // effettua process ritorno ricette
                    await ProcessRecipeFileRetAsync();
                }
                else if (ciclo == gatherCycle.VLF)
                {
                    processRecipeSyncArch();
                }

                //// provo a riconnettere SE abilitato tryRestart...
                //if (adpTryRestart && !connectionOk)
                //{
                //    // controllo se sia scaduto periodi di veto al tryConnect...
                //    int waitRecMSec = utils.CRI("waitRecMSec");
                //    // cerco se ci sia un valore in ovverride x il singolo IOB...
                //    if (IOBConfFull.General.WaitRecMsec > 0)
                //    {
                //        waitRecMSec = IOBConfFull.General.WaitRecMsec;
                //    }
                //    DateTime dtVeto = lastConnectTry.AddMilliseconds(waitRecMSec);
                //    if (DateTime.Now > dtVeto)
                //    {
                //        lgInfo($"Veto Time Elapsed | lastConnectTry: {lastConnectTry} | waitRecMSec {waitRecMSec} ms) | NOW tryConnect");
                //        lastConnectTry = DateTime.Now;
                //        tryConnect();
                //    }
                //}
                //currDispData.semIn = Semaforo.SR;
                //processDisconnectedTask();
                //processMemoryDiscon();
            }
            raiseRefresh(currDispData);
        }

        public void ExecServerRequests()
        {
            // verifica se ci siano richieste da eseguire
            if (QueueSrvReq.Count > 0)
            {
                if (MPOnline)
                {
                    if (IobOnline)
                    {
                        // prendo elenco
                        List<string> listaValori = QueueSrvReq.ToList();

                        foreach (var rawJob in listaValori)
                        {
                            // deserializzo...
                            JobTaskData jobTaskReq = JsonConvert.DeserializeObject<JobTaskData>(rawJob);
                            // processo!
                            var reqDict = JobTaskData.TaskDict(jobTaskReq.RawData);
                            if (reqDict.Count > 0)
                            {
                                var taskDone = ProcessTask(JobTaskData.TaskDict(jobTaskReq.RawData), jobTaskReq.CodTav);
                                // accodo task eseguiti...
                                string serVal = JsonConvert.SerializeObject(taskDone);
                                accodaServResp(jobTaskReq.CodTav, serVal);
                            }
                        }

                        // svuoto!
                        QueueSrvReq = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueServResp", IOBConfFull.General.EnabRedisQue, redisMan);
                    }
                }
            }
        }

        /// <summary>
        /// Esecuzione dei task richiesti e pulizia coda richieste eseguite
        /// </summary>
        /// <param name="task2exe">Elenco task da eseguire</param>
        /// <param name="codTav">Codice TAV (per macchine multi pallet) - opzionale</param>
        public virtual Dictionary<string, string> executeTasks(Dictionary<string, string> task2exe, string codTav)
        {
            string logMsg = $"Generic: call executeTasks | {task2exe.Count} task";
            if (!string.IsNullOrEmpty(codTav))
            {
                logMsg += $" | codTav: {codTav}";
            }
            lgInfo(logMsg);
            // Verificare il protocollo: dovrebbe togliere SOLO i task eseguiti...
            Dictionary<string, string> taskDone = new Dictionary<string, string>();
            if (task2exe != null)
            {
                // controllo se memMap != null...
                if (memMap != null)
                {
                    bool taskOk = false;
                    string taskVal = "";
                    string newVal = "";
                    // cerco task specifici: se ho startSetup --> imposto bit DBB701.DBB0.4
                    foreach (var item in task2exe)
                    {
                        taskOk = false;
                        taskVal = "";
                        // converto richiesta in enum...
                        taskType tName = taskType.nihil;
                        Enum.TryParse(item.Key, out tName);
                        string iKey = item.Key;
                        // se è DP aggiungo in chiave il valore della TAV richiesta... ad es setComm --> setComm#TAV_1
                        if (!string.IsNullOrEmpty(codTav))
                        {
                            iKey += $"#{codTav}";
                        }
                        // controllo sulla KEY...
                        switch (tName)
                        {
                            case taskType.setArt:
                            case taskType.setComm:
                            case taskType.setProg:
                            case taskType.setPzComm:
                                // recupero dati da memMap...
                                if (memMap != null && memMap.mMapWrite != null)
                                {
                                    if (memMap.mMapWrite.ContainsKey(iKey))
                                    {
                                        dataConf currMem = memMap.mMapWrite[iKey];
                                        string addr = currMem.memAddr;
                                        taskVal = $"SET task: {iKey} --> {item.Value} | mem: {currMem.memAddr} - {currMem.size} byte";
                                        // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà
                                        memMap.mMapWrite[iKey].value = item.Value;
                                    }
                                    else
                                    {
                                        taskVal = $"NO DATA MEM, SET task: {iKey} --> {item.Value}";
                                    }
                                }
                                else
                                {
                                    taskVal = $"NO BankConf found, SET task: {iKey} --> {item.Value}";
                                }
                                // salvo in currProd..
                                upsertKey(iKey, item.Value);

                                break;

                            case taskType.endProd:
                                // reset contapezzi inizio setup
                                if (IOBConfFull.Counters.ResetOnProdEnd)
                                {
                                    lgInfo($"Generic.executeTasks {tName} | resetContapezziPLC");
                                    taskOk = resetContapezziPLC(codTav);
                                }
                                break;

                            case taskType.forceResetPzCount:
                                // recupero dati da memMap...
                                if (memMap != null && memMap.mMapWrite != null)
                                {
                                    lgInfo($"Generic.executeTasks {tName} | resetContapezziPLC | memMap");
                                    if (memMap.mMapWrite.ContainsKey(iKey))
                                    {
                                        dataConf currMem = memMap.mMapWrite[iKey];
                                        string addr = currMem.memAddr;
                                        taskVal = $"SET task: {iKey} --> {item.Value} | mem: {currMem.memAddr} - {currMem.size} byte";
                                        // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà
                                        memMap.mMapWrite[iKey].value = item.Value;
                                        taskOk = true;
                                    }
                                    else
                                    {
                                        taskVal = $"NO DATA MEM, SET task: {iKey} --> {item.Value}";
                                    }
                                    // imposto reset gestione pzcount da parametri
                                    vetoQueuePzCount = DateTime.Now.AddMinutes(IOBConfFull.General.DelayReadPzCountSetup);
                                    dtVetoReadPzCount = DateTime.Now.AddMinutes(delayMinReadPzCount);
                                }
                                else
                                {
                                    // reset contapezzi senza memMap
                                    lgInfo($"Generic.executeTasks {tName} | resetContapezziPLC | NO memMap");
                                    taskOk = resetContapezziPLC(codTav);
                                    taskVal = taskOk ? "forceResetPzCount | RESET PZ COUNT OK" : "forceResetPzCount | PZ RESET DISABLED | NO EXEC";
                                }
                                lgInfo($"Chiamata forceResetPzCount: taskOk: {taskOk} | taskVal: {taskVal}");
                                break;

                            case taskType.forceSetPzCount:
                                // recupero dati da memMap...
                                if (memMap != null && memMap.mMapWrite != null)
                                {
                                    if (memMap.mMapWrite.ContainsKey(iKey))
                                    {
                                        dataConf currMem = memMap.mMapWrite[iKey];
                                        string addr = currMem.memAddr;
                                        taskVal = $"SET task: {iKey} --> {item.Value} | mem: {currMem.memAddr} - {currMem.size} byte";
                                        // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà
                                        memMap.mMapWrite[iKey].value = item.Value;
                                    }
                                    else
                                    {
                                        taskVal = $"NO DATA MEM, SET task: {iKey} --> {item.Value}";
                                    }
                                }
                                else
                                {
                                    taskVal = $"NO BankConf found, SET task: {iKey} --> {item.Value}";
                                }

                                lgInfo($"Chiamata forceSetPzCount: taskVal: {taskVal}");
                                break;

                            case taskType.setArtNum:
                                // in primis faccio una chiamata per tutta la tab SE fosse vuoto il
                                // dict di traduzione
                                if (DictNumArt == null || DictNumArt.Count == 0)
                                {
                                    getNumArt("");
                                }
                                // chiamo server x avere decodifica valore INT
                                newVal = getNumArt(item.Value);
                                // procedo come il resto cercando mappatura in memMap: recupero dati
                                // da memMap...
                                if (memMap != null && memMap.mMapWrite != null)
                                {
                                    if (memMap.mMapWrite.ContainsKey(iKey))
                                    {
                                        dataConf currMem = memMap.mMapWrite[iKey];
                                        string addr = currMem.memAddr;
                                        taskVal = $"SET task: {iKey} --> {newVal} | mem: {currMem.memAddr} - {currMem.size} byte";
                                        // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà
                                        memMap.mMapWrite[iKey].value = newVal;
                                    }
                                    else
                                    {
                                        taskVal = $"NO DATA MEM, SET task: {iKey} --> {newVal} ({item.Value})";
                                    }
                                }
                                else
                                {
                                    taskVal = $"NO BankConf found, SET task: {iKey} --> {newVal} ({item.Value})";
                                }

                                // salvo in currProd..
                                upsertKey(iKey, newVal);
                                break;

                            case taskType.setCommNum:
                                // chiamo server x avere decodifica valore INT
                                newVal = getNumComm(item.Value);
                                // procedo come il resto cercando mappatura in memMap: recupero dati
                                // da memMap...
                                if (memMap != null && memMap.mMapWrite != null)
                                {
                                    if (memMap.mMapWrite.ContainsKey(iKey))
                                    {
                                        dataConf currMem = memMap.mMapWrite[iKey];
                                        string addr = currMem.memAddr;
                                        taskVal = $"SET task: {iKey} --> {newVal} | mem: {currMem.memAddr} - {currMem.size} byte";
                                        // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà
                                        memMap.mMapWrite[iKey].value = newVal;
                                    }
                                    else
                                    {
                                        taskVal = $"NO DATA MEM, SET task: {iKey} --> {newVal} ({item.Value})";
                                    }
                                }
                                else
                                {
                                    taskVal = $"NO BankConf found, SET task: {iKey} --> {newVal} ({item.Value})";
                                }

                                // salvo in currProd..
                                upsertKey(iKey, newVal);
                                break;

                            case taskType.startSetup:
                                // reset contapezzi inizio setup
                                if (IOBConfFull.Counters.ResetOnSetupStart)
                                {
                                    lgDebug($"Generic.executeTasks {tName} | resetContapezziPLC");
                                    taskOk = resetContapezziPLC(codTav);
                                    vetoQueuePzCount = DateTime.Now.AddMinutes(IOBConfFull.General.DelayReadPzCountSetup);
                                    dtVetoReadPzCount = DateTime.Now.AddMinutes(delayMinReadPzCount);
                                }
                                taskVal = taskOk ? "startSetup | RESET: SETUP START" : "startSetup | PZ RESET DISABLED | NO EXEC";
                                lgInfo($"Chiamata startSetup: taskOk: {taskOk} | taskVal: {taskVal}");
                                break;

                            case taskType.stopSetup:
                                // reset contapezzi fine setup SE ESPLICITAMENTE IMPOSTATO
                                if (IOBConfFull.Counters.ResetOnSetupStop)
                                {
                                    lgDebug($"Generic.executeTasks {tName} | resetContapezziPLC");
                                    taskOk = resetContapezziPLC(codTav);
                                }
                                taskVal = taskOk ? "stopSetup | RESET: SETUP END" : "stopSetup | PZ RESET DISABLED | NO EXEC";
                                lgInfo($"Chiamata stopSetup: taskOk: {taskOk} | taskVal: {taskVal}");
                                break;

                            case taskType.setParameter:
                                // richiedo da URL i parametri WRITE da popolare
                                lgInfo("Chiamata setParameter --> processMemWriteRequests");
                                taskVal = processMemWriteRequests();
                                // se restituiscce "" faccio altra prova...
                                if (string.IsNullOrEmpty(taskVal))
                                {
                                    // i parametri me li aspetto come stringa composta paramName|paramvalue
                                    if (item.Value.Contains("|"))
                                    {
                                        string[] paramsJob = item.Value.Split('|');
                                        taskVal = $"REQUEST SET PARAMETERS: {paramsJob[0]} --> {paramsJob[1]}";
                                    }
                                    else
                                    {
                                        taskVal = $"WRONG REQUEST FOR SET PARAMETERS: {item.Value} doesnt contain pipe for splitting key/value";
                                    }
                                }
                                break;

                            case taskType.syncDbData:
                                ProcessDataSync();
                                break;

                            case taskType.processOtherInfo:
                                bool okProc = ProcessOtherInfo(iKey, item.Value);
                                taskVal = okProc ? $"OK ProcessOtherInfoAsync | {iKey} | {item.Value}" : $"ERROR ProcessOtherInfoAsync | {iKey} | {item.Value}";
#if false
                                try
                                {
                                    Task.Run(async () => okProc = await ProcessOtherInfoAsync(iKey, item.Value))
                                           .GetAwaiter()
                                           .GetResult();
                                    taskVal = okProc ? $"OK ProcessOtherInfoAsync | {iKey} | {item.Value}" : $"ERROR ProcessOtherInfoAsync | {iKey} | {item.Value}";
                                }
                                catch (Exception ex)
                                {
                                    lgError("ProcessOtherInfoAsync | Crash nel ponte Sync/Async: " + ex.Message);
                                }
#endif
                                break;

                            default:
                                taskVal = $"taskReq: {tName} | key: {iKey} | val: {item.Value} | SKIPPED | NO EXEC";
                                lgInfo($"Chiamata senza processing: taskOk: {taskOk} | taskVal: {taskVal}");
                                break;
                        }
                        // aggiungo task!
                        taskDone.Add(item.Key, taskVal);
                    }
                }
                else
                {
                    foreach (var item in task2exe)
                    {
                        // converto richiesta in enum...
                        taskType tName = taskType.nihil;
                        Enum.TryParse(item.Key, out tName);
                        string taskVal = $"taskReq: {tName} | key: {item.Key} | val: {item.Value} | SKIPPED (no BankConf) | NO EXEC";
                        // aggiungo task!
                        taskDone.Add(item.Key, taskVal);
                    }
                    lgError($"Attenzione! memMap è nullo, non posso eseguire task2exe! Tutte le richieste sono state chiuse senza esecuzione");
                }
            }

            return taskDone;
        }

        /// <summary>
        /// Cerca parametri opzionali in modalità "like" del nome
        /// </summary>
        /// <param name="keyStartSearch"></param>
        /// <returns></returns>
        public Dictionary<string, string> findOptPar(string keyStartSearch = "")
        {
            Dictionary<string, string> answ = new Dictionary<string, string>();
            // controllo SE keySearch !=""
            if (!string.IsNullOrWhiteSpace(keyStartSearch))
            {
                if (IOBConfFull.OptPar.Count > 0)
                {
                    // ciclo su tutti e cerco occorrenze che INIZINO...
                    foreach (var item in IOBConfFull.OptPar)
                    {
                        if (item.Key.StartsWith(keyStartSearch))
                        {
                            answ.Add(item.Key, item.Value);
                        }
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// forza reset ODL
        /// </summary>
        public void forceResetOdl()
        {
            lgInfo("Registrato richiesta forzatura reset ODL");
            pzCountResetted = true;
        }

        /// <summary>
        /// Effettua chiamata x split ODL
        /// </summary>
        /// <returns></returns>
        public async Task<bool> forceSplitOdl()
        {
            bool fatto = false;
            if (vetoSplit < DateTime.Now)
            {
                lgInfo("Richiesto forceSplitOdl");
                // imposto veto x 1 minuto ad altre chiamate...
                vetoSplit = DateTime.Now.AddMinutes(1);
                // eseguo SOLO SE sono online...
                if (MPOnline && IobOnline)
                {
                    string fullUrl = "";
                    string rawSplit = "";
                    try
                    {
                        /***************************************************
                        * Descrizione procedura (OK X SIMULATORI... da provare x DP come OpcUaSiemensSW)
                        *
                        * - chiamata su MP/IO
                        * - verifica che su DB sia abilitato AUTO ODL
                        * - il server inserisce un evento fine prod HW 1 minuto prima e inizio setup HW
                        * - viene duplicato e chiuso ODL corrente
                        * - viene fatto partire ODL nuovo ADESSO
                        * - num pezzi come ODL precedente (o da media 3 ODL precedenti)
                        * - conferme pezzi & co... gestione NULL (NON SERVONO si tratta di impianti SENZA gestione vera ODL)
                        * - reset contapezzi PLC locale...
                        *
                        *
                        *
                        * DA VALUTARE (x macchine tipo linea con + impianti... es valvital) SE
                        * - creare una gestione ALTERNATIVA sul server che preveda impianto LEADER e impianti follower (RIGIDAMENTE CONFIGURATI)
                        * - ogni volta che si fa setup LEADER --> si ripete su impianti FOLLOWER (eventi!!!)
                        * - viene okReport reset contapezzi sui follower (+ altre operazioni opzionali, ES imposstazione nome commessa, quantità, articolo...)
                        * - viene okReport reset + nuovo ODL (con stessi articoli e quantità) su follower, SENZA avere gestione x ODL di un codice esterno (quindi registra TUTTO ma NON RITORNERA' dati non avendo link verso esterno)
                        * - serve NUOVA TABELLA delle macchine LEADER | FOLLOWER (1:n) e gestione da MP/IO
                        *
                        * ***************************************************/

                        // se normale splitto!
                        if (!isMulti)
                        {
                            // invio chiamata URL x reset ODL su macchina
                            rawSplit = await callUrl(urlForceSplit, false);
                            fatto = (rawSplit != "KO") ? true : false;
                        }
                        // se multi gestisco il bit delle tavole...
                        else
                        {
                            foreach (string item in IOBConfFull.Device.MultiIobList)
                            {
                                // invio chiamata URL x reset ODL su macchina, ATTENZIONE scriviamo
                                // | al posto di "#" che in URL sarebbe filtrato...
                                fullUrl = $"{urlForceSplit}&multi={item}";
                                rawSplit = await callUrl(fullUrl, false);
                                lgDebug($"Esecuzione forceSplit | URL: {fullUrl} | esito: {rawSplit}");
                            }
                            fatto = (rawSplit == "OK") ? true : false;
                        }
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in forceSplitOdl{Environment.NewLine}{exc}");
                    }
                    // se okReport --> resetto contapezzi!!!
                    if (fatto)
                    {
                        contapezziPLC = 0;
                        contapezziIOB = 0;
                    }
                }
                else
                {
                    lgError("Richiesto forceSplitOdl ma MP/IOB offline --> NON eseguito");
                }
            }
            else
            {
                lgError("Richiesto forceSplitOdl ma veto attivo --> NON eseguito");
            }
            return fatto;
        }

        /// <summary>
        /// Processing degli allarmi (se presenti e gestiti)
        /// </summary>
        /// <returns></returns>
        public virtual Dictionary<string, string> getAlarmData()
        {
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            // ora aggiungo (se ci fossero) gli allarmi...
            if (alarmMaps != null && alarmMaps.Count > 0)
            {
                if (hasAlarms())
                {
                    string bankVal = "";
                    foreach (var item in alarmMaps)
                    {
                        bankVal = "";
                        // verifico ed eseguo secondo il tipo di allarme gestito...
                        if (alarmType == AlarmBlockType.Bitmap)
                        {
                            var currState = currAlarmsState(item);
                            // calcolo vettore stringa degli allarmi...
                            bankVal = getAlarmState(item, currState);
                        }
                        else if (alarmType == AlarmBlockType.ActiveList)
                        {
                            // cerco se sia superiore al livello minimo da conf
                            if (item.blockLevel >= alarmLevelMin)
                            {
                                int numActive = getAlarmStatus(item);
                                // calcolo vettore stringa degli allarmi...
                                bankVal = getAlarmState(item, numActive);
                            }
                        }
                        // sistemo se OK e/o invio
                        bankVal = string.IsNullOrEmpty(bankVal) ? "OK" : bankVal;
                        saveAlarmString(ref outVal, bankVal, item.description);
                    }
                }
            }
            else
            {
                lgTrace("No alarm found in alarmMaps");
            }

            if (periodicLog || outVal.Count > 0)
            {
                lgDebug($"Esito getAlarmData: {outVal.Count} allarmi attivi/validi in outVal");
            }
            return outVal;
        }

        /// <summary>
        /// Effettua conversione da valore bitmap ai valori configurati x allarme
        /// </summary>
        /// <param name="memConf">Configurazione banco allarmi</param>
        /// <param name="bState">BitState allarmi</param>
        /// <returns></returns>
        public string getAlarmState(BaseAlarmConf memConf, byte[] bState)
        {
            string valTransl = "";
            List<string> descAttivi = new List<string>();
            BitArray bitAttivi = new BitArray(bState);
            bool[] bitStati = new bool[bitAttivi.Count];
            bitAttivi.CopyTo(bitStati, 0);
            // cerco bit attivi
            int idx = 0;
            foreach (var item in bitStati)
            {
                if (item && memConf.messages.Count > idx)
                {
                    string currState = memConf.messages[idx];
                    descAttivi.Add(currState);
                }
                idx++;
            }
            // combino string
            valTransl = string.Join(",", descAttivi);
            return valTransl;
        }

        /// <summary>
        /// Effettua conversione tra gli allarmi attivi secondo configurazione banco allarmi
        /// </summary>
        /// <param name="memConf">Configurazione banco allarmi</param>
        /// <param name="numAlarms">num allarmi attivi</param>
        /// <returns></returns>
        public virtual string getAlarmState(BaseAlarmConf memConf, int numAlarms)
        {
            string valTransl = "";
            List<string> descAttivi = new List<string>();

            // FAKE!!!: dovrebbe cercare in aree memoria x ogni valore configurato, vedere
            // implementazione specifica es OpcUa

            // combino string
            valTransl = string.Join(",", descAttivi);
            return valTransl;
        }

        /// <summary>
        /// Effettua conversione da valore bitmap ai valori configurati
        /// </summary>
        /// <param name="memConf"></param>
        /// <param name="bState"></param>
        /// <returns></returns>
        public string getBitmapState(dataConfTSVC memConf, byte[] bState)
        {
            string valTransl = "";
            List<string> descAttivi = new List<string>();
            BitArray bitAttivi = new BitArray(bState);
            bool[] bitStati = new bool[bitAttivi.Count];
            bitAttivi.CopyTo(bitStati, 0);
            // cerco bit attivi
            int idx = 0;
            foreach (var item in bitStati)
            {
                if (item && memConf.decodeMap.Count > idx)
                {
                    string currState = memConf.decodeMap[idx];
                    descAttivi.Add(currState);
                }
                idx++;
            }
            // combino string
            valTransl = string.Join(",", descAttivi);
            return valTransl;
        }

        /// <summary>
        /// Recupera eventuali allarmi CNC...
        /// </summary>
        public virtual Dictionary<string, string> getCncAlarms()
        {
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            return outVal;
        }

        /// <summary>
        /// Restituisce info DINAMICHE
        /// </summary>
        /// <returns></returns>
        public virtual Dictionary<string, string> getDynData()
        {
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            return outVal;
        }

        /// <summary>
        /// Cerca se esiste il parametro opzionale nei KVP (JSON) e lo restituisce
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string getOptJsonKVP(string key)
        {
            string answ = "";
            // controllo SE HO il parametro
            if (memMap != null && memMap.OptKVP != null && memMap.OptKVP.Count > 0)
            {
                if (memMap.OptKVP.ContainsKey(key))
                {
                    answ = memMap.OptKVP[key];
                }
            }
            return answ;
        }

        /// <summary>
        /// Cerca se esiste il parametro opzionale e lo restituisce
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string getOptPar(string key)
        {
            return IOBConfFull.OptParGet(key);
        }

        /// <summary>
        /// Cerca se esiste un link tra aree di memoria in write e lo restituisce
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string getOptWriteLink(string key)
        {
            string answ = "";
            // controllo SE HO il parametro
            if (memMap != null && memMap.mMapWriteLink != null && memMap.mMapWriteLink.Count > 0)
            {
                if (memMap.mMapWriteLink.ContainsKey(key))
                {
                    answ = memMap.mMapWriteLink[key];
                }
            }
            return answ;
        }

        /// <summary>
        /// Restituisce info OVERRIDES
        /// </summary>
        /// <returns></returns>
        public virtual Dictionary<string, string> getOverrides()
        {
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            return outVal;
        }

        /// <summary>
        /// Restituisce programma in esecuzione
        /// </summary>
        public virtual string getPrgName()
        {
            return "";
        }

        /// <summary>
        /// Restituisce info sistema
        /// </summary>
        /// <returns></returns>
        public virtual Dictionary<string, string> getSysInfo()
        {
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            return outVal;
        }

        /// <summary>
        /// Recupera la VC x TS, svuotando lista e resettando periodo partenza
        /// </summary>
        /// <param name="VCName">Nome della VC</param>
        /// <param name="doReset">Reimposta e resetta array VC</param>
        /// <returns></returns>
        public double getVal_TSVC(string VCName, bool doReset)
        {
            double answ = -999999;
            // cerco VC...
            if (TSVC_Data.ContainsKey(VCName))
            {
                try
                {
                    switch (TSVC_Data[VCName].Funzione)
                    {
                        case VC_func.POINT:
                            // prendo PRIMO
                            answ = TSVC_Data[VCName].dataArray.FirstOrDefault();
                            break;

                        case VC_func.AVG:
                            answ = TSVC_Data[VCName].dataArray.Average();
                            break;

                        case VC_func.MEDIAN:
                            answ = TSVC_Data[VCName].dataArray.Median();
                            break;

                        case VC_func.MIN:
                            answ = TSVC_Data[VCName].dataArray.Min();
                            break;

                        case VC_func.MAX:
                        default:
                            answ = TSVC_Data[VCName].dataArray.Max();
                            break;
                    }
                }
                catch
                { }
                // ora resetto... SE richiesto...
                if (doReset)
                {
                    TSVC_Data[VCName].dataArray = new List<double>();
                    TSVC_Data[VCName].DTStart = DateTime.Now;
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera la VC x TS, svuotando lista e resettando periodo partenza
        /// </summary>
        /// <param name="VCName">Nome della VC</param>
        /// <param name="doReset">Reimposta e resetta array VC</param>
        /// <returns></returns>
        public int getVal_TSVC_int(string VCName, bool doReset)
        {
            int answ = 0;
            // cerco VC...
            if (TSVC_Data.ContainsKey(VCName))
            {
                // !!!FARE!!! vero calcolo... x ora FIX a MAX...
                foreach (var item in TSVC_Data[VCName].dataArray)
                {
                    answ = (int)item > answ ? (int)item : answ;
                }
                // ora resetto... SE richiesto..
                if (doReset)
                {
                    TSVC_Data[VCName].dataArray = new List<double>();
                    TSVC_Data[VCName].DTStart = DateTime.Now;
                }
            }
            return answ;
        }

        /// <summary>
        /// Area init asincrono (fare override async!o)
        /// </summary>
        /// <returns></returns>
        public virtual Task InitializeAsync()
        {
            // da usare per implementare logiche di init specifiche
            return Task.CompletedTask;
        }

        /// <summary>
        /// Restituisce un payload in formato json della lista di valori ricevuta
        /// </summary>
        /// <param name="tipoUrl">Tipo di URL (eventi / FLog)</param>
        /// <param name="elencoValori">elenco di valori da coda string salvata</param>
        /// <returns></returns>
        public string jsonPayload(urlType tipoUrl, List<string> elencoValori)
        {
            string answ = "";
            if (elencoValori != null)
            {
                string[] valori;
                int counter = 0;
                DateTime dtEve = DateTime.Now;
                switch (tipoUrl)
                {
                    case urlType.FLog:
                        flogData currFlData = new flogData();
                        flogJsonPayload fullFlObj = new flogJsonPayload();
                        fullFlObj.fluxData = new List<flogData>();
                        // inizio processando ogni valore
                        foreach (var item in elencoValori)
                        {
                            if (item != null)
                            {
                                valori = qDecodeIN(item);
                                CultureInfo provider = CultureInfo.InvariantCulture;
                                DateTime.TryParseExact(valori[0], "yyyyMMddHHmmssfff", provider, DateTimeStyles.AssumeLocal, out dtEve);
                                int.TryParse(valori[3], out counter);
                                currFlData = new flogData()
                                {
                                    flux = valori[1],
                                    valore = valori[2],
                                    dtEve = dtEve,
                                    dtCurr = DateTime.Now,
                                    cnt = counter
                                };
                                fullFlObj.fluxData.Add(currFlData);
                            }
                        }
                        // conversione finale
                        try
                        {
                            answ = JsonConvert.SerializeObject(fullFlObj);
                        }
                        catch (Exception exc)
                        {
                            lgError($"FLog Errore in costruzione jsonPayload:{Environment.NewLine}{exc}");
                        }
                        break;

                    case urlType.SignIN:
                        evData currSigData = new evData();
                        evJsonPayload fullEvObj = new evJsonPayload();
                        fullEvObj.eventList = new List<evData>();
                        // inizio processando ogni valore
                        foreach (var item in elencoValori)
                        {
                            valori = qDecodeIN(item);
                            CultureInfo provider = CultureInfo.InvariantCulture;
                            DateTime.TryParseExact(valori[0], "yyyyMMddHHmmssfff", provider, DateTimeStyles.AssumeLocal, out dtEve);
                            int.TryParse(valori[2], out counter);
                            currSigData = new evData()
                            {
                                valore = valori[1],
                                dtEve = dtEve,
                                dtCurr = DateTime.Now,
                                cnt = counter
                            };
                            fullEvObj.eventList.Add(currSigData);
                        }
                        // conversione finale
                        try
                        {
                            answ = JsonConvert.SerializeObject(fullEvObj);
                        }
                        catch (Exception exc)
                        {
                            lgError($"SignIN Errore in costruzione jsonPayload:{Environment.NewLine}{exc}");
                        }
                        break;

                    case urlType.RawTransf:
                        BaseRawTransf currRTData = new BaseRawTransf();
#if false
                        rawTransfJsonPayload fullRTObj = new rawTransfJsonPayload();
                        fullRTObj.rawTransfData = new List<BaseRawTransf>();
                        // inizio processando ogni valore
                        foreach (var item in elencoValori)
                        {
                            try
                            {
                                currRTData = JsonConvert.DeserializeObject<BaseRawTransf>(item);
                            }
                            catch (Exception exc)
                            {
                                lgError($"Eccezione in deserializzazione BaseRawTransf:{Environment.NewLine}{exc}");
                            }
                            fullRTObj.rawTransfData.Add(currRTData);
                        }
                        // conversione finale
                        try
                        {
                            answ = JsonConvert.SerializeObject(fullRTObj);
                        }
                        catch (Exception exc)
                        {
                            lgError($"RawTransf Errore in costruzione jsonPayload:{Environment.NewLine}{exc}");
                        }
#endif

                        // provo una serializzazione "brutale", ovvero aggiungo alla stringa il
                        // valore di testa...
                        string rawResult = "";
                        foreach (var item in elencoValori)
                        {
                            rawResult += $"{item},";
                        }
                        if (rawResult.Length > 0)
                        {
                            rawResult = rawResult.Substring(0, rawResult.Length - 1);
                        }
                        answ = $"[{rawResult}]";

                        //hasVeto = "{" + $"\"rawTransfData\":[{rawResult}]" + "}";

                        break;

                    case urlType.ULog:
                        int numVal = 0;
                        int matrOp = 0;
                        ulogData currUlData = new ulogData();
                        ulogJsonPayload fullUlObj = new ulogJsonPayload();
                        fullUlObj.fluxData = new List<ulogData>();
                        // inizio processando ogni valore
                        foreach (var item in elencoValori)
                        {
                            valori = qDecodeIN(item);
                            CultureInfo provider = CultureInfo.InvariantCulture;
                            DateTime.TryParseExact(valori[0], "yyyyMMddHHmmssfff", provider, DateTimeStyles.AssumeLocal, out dtEve);
                            int.TryParse(valori[3], out matrOp);
                            int.TryParse(valori[5], out numVal);
                            int.TryParse(valori[6], out counter);
                            currUlData = new ulogData()
                            {
                                flux = valori[1],
                                valore = valori[2],
                                dtEve = dtEve,
                                dtCurr = DateTime.Now,
                                cnt = counter,
                                matrOpr = matrOp,
                                label = valori[4],
                                valNum = numVal
                            };
                            fullUlObj.fluxData.Add(currUlData);
                        }
                        // conversione finale
                        try
                        {
                            answ = JsonConvert.SerializeObject(fullUlObj);
                        }
                        catch (Exception exc)
                        {
                            lgError($"ULog Errore in costruzione jsonPayload:{Environment.NewLine}{exc}");
                        }
                        break;

                    default:
                        break;
                }
            }
            return answ;
        }

        /// <summary>
        /// Restitusice valore ultimo invio di una chiave, se non esistesse fornisce valore di 1 gg prima e lo salva...
        /// </summary>
        /// <param name="keyReq"></param>
        /// <returns></returns>
        public DateTime LastSendGet(string keyReq)
        {
            DateTime lastSend = DateTime.Now.AddDays(-1);
            string lastSendKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:LastSend");
            string rawVal = redisMan.redGetHashField(lastSendKey, keyReq);
            if (!string.IsNullOrEmpty(rawVal))
            {
                lastSend = JsonConvert.DeserializeObject<DateTime>(rawVal);
            }
            else
            {
                rawVal = JsonConvert.SerializeObject(lastSend);
                KeyValuePair<string, string>[] hashFields = new KeyValuePair<string, string>[1];
                hashFields[0] = new KeyValuePair<string, string>(keyReq, rawVal);
                redisMan.redSaveHash(lastSendKey, hashFields);
            }
            return lastSend;
        }

        /// <summary>
        /// Imposta valore datetime x ultimo invio di una chiamata in REDIS HashSet
        /// </summary>
        /// <param name="keyReq"></param>
        /// <param name="dtRif"></param>
        public bool LastSendSet(string keyReq, DateTime dtRif)
        {
            string lastSendKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:LastSend");
            string rawVal = JsonConvert.SerializeObject(dtRif);
            KeyValuePair<string, string>[] hashFields = new KeyValuePair<string, string>[1];
            hashFields[0] = new KeyValuePair<string, string>(keyReq, rawVal);
            bool fatto = redisMan.redSaveHash(lastSendKey, hashFields);
            return fatto;
        }

        /// <summary>
        /// Effettua un trim della stringa al numero max di linee da mostrare a video
        /// </summary>
        /// <param name="newString"></param>
        /// <returns></returns>
        public string limitLine2show(string newString)
        {
            // se num righe superiore a limite trimmo...
            if (newString.Split('\n').Length > parentForm.nLine2show)
            {
                //int idx = newString.LastIndexOf('\r');
                int idx = newString.LastIndexOf(Environment.NewLine);
                newString = newString.Substring(0, idx);
            }
            return newString;
        }

        /// <summary>
        /// riporta il log di tutti i dati di results temporali registrati
        /// </summary>
        public void logTimeResults()
        {
            if (TimingData.results.Count > 0)
            {
                lgInfo("{0}--------------- START TIMING DATA ---------------", Environment.NewLine);
                int globNumCall = 0;
                TimeSpan globAvgMsec = new TimeSpan(0);
                foreach (TimeRec item in TimingData.results)
                {
                    // loggo SOLO se del mio IOB corrente...
                    if (item.classCall == IOBConfFull.General.FilenameIOB)
                    {
                        lgInfo("{4}|Chiamate {0}: effettuate {1}, tempo medio {2:N2} msec | impegno canale {3:P3}", item.codCall, item.numCall, item.avgMsec, item.totMsec.TotalSeconds / DateTime.Now.Subtract(dtAvvioAdp).TotalSeconds, IOBConfFull.General.CodIOB);
                        globNumCall += item.numCall;
                        globAvgMsec += item.totMsec;
                    }
                }
                // riporto conteggio medio al secondo...
                lgInfo("{4}|Chiamate GLOBALI: {0}, periodo: {1:N2} minuti.cent, tempo medio {2:N2} msec | impegno canale {3:P3}", globNumCall, DateTime.Now.Subtract(dtAvvioAdp).TotalMinutes, globAvgMsec.TotalMilliseconds / globNumCall, globAvgMsec.TotalSeconds / DateTime.Now.Subtract(dtAvvioAdp).TotalSeconds, IOBConfFull.General.CodIOB);
                lgInfo("{0}--------------- STOP TIMING DATA ---------------{0}", Environment.NewLine);
                // mostro in form statistiche globali!
                parentForm.updateComStats(string.Format("Periodo: {0:N2}min | {1} x {2:N2}ms | canale {3:P3}", DateTime.Now.Subtract(dtAvvioAdp).TotalMinutes, globNumCall, globAvgMsec.TotalMilliseconds / globNumCall, globAvgMsec.TotalSeconds / DateTime.Now.Subtract(dtAvvioAdp).TotalSeconds));
            }
        }

        /// <summary>
        /// Processa un monitoredItem, e ritorna boolean SE richiede invio (cambio o scadenza)
        /// </summary>
        /// <param name="newVal"></param>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool monItem2Send(string newVal, DynDataItem item)
        {
            bool answ = false;
            if (item != null)
            {
                // controllo in base al tipo di function...
                switch (item.func)
                {
                    case "SAMPLE":
                        // controllo se scaduto sample period...
                        if (item.DTScad < DateTime.Now)
                        {
                            answ = true;
                        }
                        break;

                    case "CHANGE":
                    default:
                        // controllo se scaduto o se variato...
                        if (newVal != item.actVal || item.DTScad < DateTime.Now)
                        {
                            // aggiorno scadenza e che vada inviato
                            answ = true;
                        }
                        break;
                }
            }
            return answ;
        }

        /// <summary>
        /// Verifica e processing x gestione Dossier quotidiani automatica
        /// </summary>
        public void ProcessAutoDossier()
        {
            bool fatto = false;
            string callResp = "";
            if (IOBConfFull.FluxLog.AutoSnapshotDossier)
            {
                DateTime adesso = DateTime.Now;
                if (adesso > dtVetoAutoDossier)
                {
                    dtVetoAutoDossier = adesso.AddMinutes(30);
                    lgTrace("Richiesta ProcessAutoDossier");

                    lgTrace("AutoSnapshotDossier abilitato");
                    // chiamo stored x creare Snapshot Dossier giornalieri fino alla data...
                    callResp = utils.callUrl(urlFixDailyDossier);
                    fatto = callResp == "OK";
                    lgDebug($"Esecuzione ProcessAutoDossier completata --> esito: {callResp}");
                }
                else
                {
                    lgTrace("AutoSnapshotDossier DISABILITATO");
                }
            }
            // loggo se enabled
            if (fatto)
            {
                lgTrace($"Effettuato ProcessAutoDossier, esito positivo | {DateTime.Now:HH.mm.ss}");
            }
        }

        /// <summary>
        /// Wrapper AutoOdl in modalità Sync
        /// </summary>
        public void ProcessAutoOdl()
        {
            try
            {
                Task.Run(async () => await ProcessAutoOdlAsync())
                       .GetAwaiter()
                       .GetResult();
            }
            catch (Exception ex)
            {
                lgError("ProcessAutoOdl | Crash nel ponte Sync/Async: " + ex.Message);
            }
        }

        /// <summary>
        /// Verifica e processing x gestione ODL automatica
        /// </summary>
        public async Task ProcessAutoOdlAsync()
        {
            bool fatto = false;
            if (IOBConfFull.Odl.AutoChangeOdl)
            {
                lgTrace("ProcessAutoOdlAsync | AutoChangeOdl abilitato");
                // imposto il veto lettura contapezzi a 1 minuto x iniziare...
                dtVetoReadPzCount = DateTime.Now.AddMinutes(delayMinReadPzCount);
                string fullUrl = "";
                DateTime adesso = DateTime.Now;
                DateTime inizioOdl = adesso;
                string rawDataInizio = "";
                if (VetoProcessAutoOdl > adesso)
                {
                    lgTrace($"Veto per check autoOdl attivo fino a {VetoProcessAutoOdl} | salto verifica");
                }
                else
                {
                    bool callChangeODL = false;
                    DateTime dtStart = await currOdlStart();
                    switch (IOBConfFull.Odl.ChangeOdlMode)
                    {
                        case "PZCOUNT_RESET":
                            /* verifico se sia "armato" il reset cambio ODL, DEVO essere :
                            * - NON in produzione
                            * - contapezzi ACT < contapezzi LAST
                            * */
                            lgTrace("ProcessAutoOdlAsync: caso PZCOUNT_RESET");
                            if ((!isRunning || forceResetInRun) && pzCountResetted)
                            {
                                callChangeODL = true;
                                lgInfo("Attivato cambio ODL da PZCOUNT_RESET");
                            }
                            else
                            {
                                lgInfo($"isRunning: {isRunning} | pzCountResetted: {pzCountResetted} | forceResetInRun: {forceResetInRun} | contapezziIOB: {contapezziIOB} | contapezziPLC: {contapezziPLC}");
                            }
                            break;

                        case "DAILY":
                            // verifico inizio ODL, se è data di oggi NON eseguo...
                            if (dtStart.Date < adesso.Date)
                            {
                                // chiamo stored x creare ODL giornalieri alla data...
                                string autoOdlRes = utils.callUrl(urlFixDailyOdl);
                                fatto = autoOdlRes == "OK";
                                // imposto x prox controllo veto a 10 min
                                VetoProcessAutoOdl = adesso.AddMinutes(10);
                            }
                            else
                            {
                                // imposto x prox controllo veto a 30 min
                                VetoProcessAutoOdl = adesso.AddMinutes(30);
                            }
                            break;

                        case "DAILY_CONF_PZ":
                            // verifico inizio ODL, se è data di oggi NON eseguo...
                            if (dtStart.Date < adesso.Date)
                            {
                                // imposto x prox controllo veto a 10 min
                                VetoProcessAutoOdl = adesso.AddMinutes(10);
                                string autoOdlRes = "";
                                if (!isMulti)
                                {
                                    // chiamo stored x creare ODL giornalieri alla data + conferma pezzi...
                                    autoOdlRes = utils.callUrl(urlFixDailyOdlConfPzCount);
                                }
                                else
                                {
                                    // prendo il + vecchio...
                                    foreach (var item in IOBConfFull.Device.MultiIobList)
                                    {
                                        fullUrl = $@"{urlCommand("fixDailyOdlConfPzCount")}{item}";
                                        autoOdlRes = await callUrl(fullUrl, false);
                                    }
                                }
                                fatto = autoOdlRes == "OK";
                            }
                            else
                            {
                                // imposto x prox controllo veto a 30 min
                                VetoProcessAutoOdl = adesso.AddMinutes(30);
                            }
                            break;

                        case "SIMUL":
                        case "TIME":
                            // imposto x prox controllo veto a 1 min
                            VetoProcessAutoOdl = adesso.AddMinutes(1);
                            // controllo parametri validi
                            if (IOBConfFull.Odl.OdlDurationHours > 0 && IOBConfFull.Odl.IdleStateMin >= 0)
                            {
                                // leggo da server inizio ODL... se non multi 1 solo...
                                inizioOdl = DateTime.Now;
                                rawDataInizio = "";
                                if (!isMulti)
                                {
                                    rawDataInizio = await callUrl(urlInizioOdlIob, false);
                                    DateTime.TryParse(rawDataInizio, out inizioOdl);
                                }
                                else
                                {
                                    DateTime tmpData = DateTime.Now;
                                    // prendo il + vecchio...
                                    foreach (var item in IOBConfFull.Device.MultiIobList)
                                    {
                                        fullUrl = $"{urlInizioOdlIob}|{item}";
                                        rawDataInizio = await callUrl(fullUrl, false);
                                        DateTime.TryParse(rawDataInizio, out tmpData);
                                        inizioOdl = (tmpData < inizioOdl) ? tmpData : inizioOdl;
                                    }
                                }
                                // verifico se sia scaduto...
                                if (inizioOdl.AddHours(IOBConfFull.Odl.OdlDurationHours) < adesso)
                                {
                                    string rawIdle = "";
                                    int idlePeriod = 0;
                                    if (!isMulti)
                                    {
                                        // controllo SE sono fermo (spento o in manuale) per il
                                        // periodo minimo richiesto...
                                        rawIdle = await callUrl(urlIdleTime, false);
                                        int.TryParse(rawIdle, out idlePeriod);
                                    }
                                    else
                                    {
                                        int tmpIdle = 0;
                                        // prendo il + grande...
                                        foreach (var item in IOBConfFull.Device.MultiIobList)
                                        {
                                            fullUrl = $"{urlIdleTime}|{item}";
                                            rawIdle = await callUrl(fullUrl, false);
                                            int.TryParse(rawIdle, out tmpIdle);
                                            idlePeriod = tmpIdle > idlePeriod ? tmpIdle : idlePeriod;
                                        }
                                    }
                                    if (idlePeriod >= IOBConfFull.Odl.IdleStateMin)
                                    {
                                        callChangeODL = true;
                                    }
                                }
                            }
                            // se NON fosse scaduto MA è simulato esegue controllo sul num pezzi da
                            // fare, se sfora (RANDOM) > +(50...110)% --> cambia!
                            if (!callChangeODL && IOBConfFull.Odl.ChangeOdlMode == "SIMUL")
                            {
                                var rawCount = await callUrl(urlGetNumPzCurrODL, false);
                                if (int.TryParse(rawCount, out var numPzReqOdl))
                                {
                                    int limitQty = (numPzReqOdl * rndGen.Next(150, 210)) / 100;
                                    callChangeODL = contapezziPLC > limitQty;
                                }
                            }
                            break;

                        default:
                            break;
                    }

                    // vero processing...
                    if (callChangeODL)
                    {
                        lgTrace("Chiamata: ProcessAutoOdlAsync --> forceSplitODL");
                        fatto = await forceSplitOdl();
                        // metto contapezzi a zero..
                        contapezziPLC = 0;
                        // aspetto 2 sec per proseguire dopo force split...
                        await Task.Delay(2000);
                        pzCountResetted = false;
                        lgInfo("Esecuzione ProcessAutoOdlAsync completata --> pzCountResetted = false");
                        // imposto x prox controllo veto a 15 min
                        VetoProcessAutoOdl = adesso.AddMinutes(15);
                    }
                }
            }
            else
            {
                lgTrace("ProcessAutoOdlAsync | AutoChangeOdl DISABILITATO");
            }
            // loggo se ok + processing post opzionali
            if (fatto)
            {
                lgInfo($"Effettuato ProcessAutoOdlAsync | mode: {IOBConfFull.Odl.ChangeOdlMode}");
                processAutoOdlExtraStep();
            }
        }

        /// <summary>
        /// Effettua processing degli allarmi CNC SE disponibili
        /// </summary>
        public void processCncAlarms()
        {
            if (utils.CRB("enableAlarms"))
            {
                Dictionary<string, string> currAlarms = new Dictionary<string, string>();
                if (connectionOk)
                {
                    currAlarms = getCncAlarms();
                }
                else
                {
                    lgError("Errore connessione mancante x getCncAlarms");
                }
                // verifico SE sia cambiato il programma...
                if (currAlarms.Count > 0)
                {
                    try
                    {
                        string sVal = "";
                        if (lastAlarm != currAlarms["CNC_ALARM"])
                        {
                            // salvo!
                            lastAlarm = currAlarms["CNC_ALARM"];
                            // per ogni valore del dizionario mostro ed accodo!
                            foreach (var item in currAlarms)
                            {
                                // verifico NON sia un ND...
                                if (item.Key == "ND" || string.IsNullOrEmpty(item.Key))
                                {
                                    // log anomalia...
                                    lgTrace($"Errore in predisposizione FL Allarmi CNC: rawJob.key risulta ND! | rawJob.key: {item.Key} | rawJob.Value: {item.Value}");
                                }
                                else
                                {
                                    sVal = string.Format("[CNC_ALARM]{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);
                                    }
                                }
                            }
                        }
                        // accodo ALTRI allarmi NON CNC...
                        foreach (var item in currAlarms.Where(X => X.Key != "CNC_ALARM"))
                        {
                            // verifico NON sia un ND...
                            if (item.Key == "ND" || string.IsNullOrEmpty(item.Key))
                            {
                                // log anomalia...
                                lgTrace($"Errore in predisposizione FL Allarmi NON CNC: rawJob.key risulta ND! | rawJob.key: {item.Key} | rawJob.Value: {item.Value}");
                            }
                            else
                            {
                                sVal = $"{item.Key} | {item.Value}";
                                bool sent = accodaFLog(item.Key, sVal, qEncodeFLog(item.Key, item.Value));
                                if (sent)
                                {    // traccio valore DynData x analisi
                                    trackDynData(item.Key, item.Value);
                                }
                            }
                        }
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in processCncAlarms{Environment.NewLine}{exc}");
                    }
                }
            }
        }

        /// <summary>
        /// Effettua processing contapezzi (ed eventualmente alza il bit di contapezzo...)
        /// </summary>
        public virtual void processContapezzi()
        { }

        /// <summary>
        /// Effettua processing CUSTOM x l'IOB corrente (ed invia ad IO) (task svolto tipicamente
        /// ogni 5 sec se base timer 10ms, vedere app.config)
        /// </summary>
        public virtual void processCustomTaskLF()
        { }

        /// <summary>
        /// Effettua processing CUSTOM x l'IOB corrente (ed invia ad IO) (task svolto tipicamente
        /// ogni 3 sec se base timer 10ms, vedere app.config)
        /// </summary>
        public virtual void processCustomTaskMF()
        { }

        /// <summary>
        /// Task periodici SE disconnesso
        /// </summary>
        public virtual void processDisconnectedTask()
        {
        }

        /// <summary>
        /// Effettua processing del recupero dei valori dinamici:
        /// es: speed (RPM, feedrate) degli assi, valori pressioni, temeprature
        /// </summary>
        public void processDynData()
        {
            // FixMe Todo: generalizzare parametri nell'obj?
            bool enableByApp = utils.CRB("enableDynData");
            Dictionary<string, string> currDynData = new Dictionary<string, string>();

            if (enableByApp || IOBConfFull.FluxLog.EnableDynData)
            {
                // verifico se ho fattore demoltiplica...
                if (demFactDynData > 1)
                {
                    lgTrace($"Non eseguo processDynData: fatt veto {demFactDynData}");
                    // riduco...
                    demFactDynData--;
                }
                else
                {
                    lgTrace("Inizio processDynData");
                    // sistemo il valore di demoltiplica a default
                    fixDemFactDynData();

                    // proseguo
                    if (connectionOk)
                    {
                        currDynData = getDynData();
                        bool hasDynDataVal = currDynData.Count > 0;
                        if (!hasDynDataVal)
                        {
                            lgWarn($"processDynData.getDynData: nessun valori DynData ricevuto");
                        }
                        else
                        {
                            // verifico DynData siano validi: se tutti uguali NON li considero validi...
                            bool isValidSet = checkValidDynData(currDynData);
                            // se non valido loggo e NON proseguo..
                            if (!isValidSet)
                            {
                                lgWarn($"processDynData.getDynData: Valori ricevuti NON validi | # dynData{currDynData.Count}");
                            }
                            else
                            {
                                var currAlarmData = getAlarmData();
                                lgTrace($"currDynData: {currDynData.Count} | currAlarmData: {currAlarmData.Count}");
                                // se ho allarmi
                                if (currAlarmData != null && currAlarmData.Count > 0)
                                {
                                    foreach (var item in currAlarmData)
                                    {
                                        if (!currDynData.ContainsKey(item.Key))
                                        {
                                            // aggiungo!
                                            currDynData.Add(item.Key, item.Value);
                                        }
                                    }
                                }
                                // verifico parametro sintesi... che ricalcolo ogni volta
                                if (!currDynData.ContainsKey("DYNDATA") && hasDynDataVal)
                                {
                                    currDynData.Add("DYNDATA", $"{currDynData.Count}xVal");
                                }
                                // verifico se DynData sia abilitato IN GENERALE
                                if (!IOBConfFull.FluxLog.DisDynData)
                                {
                                    try
                                    {
                                        string sVal = "";
                                        // se richiesto send diretto...
                                        if (IOBConfFull.FluxLog.ForceDynData)
                                        {
                                            // per ogni valore del dizionario mostro ed accodo!
                                            foreach (var item in currDynData)
                                            {
                                                // controllo se vietato dynData
                                                if (IOBConfFull.FluxLog.SendDynDataRec || item.Key != "DYNDATA")
                                                {
                                                    // verifico NON sia un ND...
                                                    if (item.Key == "ND" || string.IsNullOrEmpty(item.Key))
                                                    {
                                                        // log anomalia...
                                                        lgTrace($"Errore in processDynData.01: rawJob.key risulta ND! | rawJob.key: {item.Key} | rawJob.Value: {item.Value}");
                                                    }
                                                    else
                                                    {
                                                        sVal = string.Format("[DYNDATA]{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);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                        // altrimenti verifico SE sia cambiato il valore dei DynData...
                                        else if (hasDynDataVal && (lastDynDataCtrlVal == null || lastDynDataCtrlVal != currDynData["DYNDATA"]))
                                        {
                                            // salvo!
                                            lastDynDataCtrlVal = currDynData["DYNDATA"];
                                            // per ogni valore del dizionario mostro ed accodo!
                                            foreach (var item in currDynData)
                                            {
                                                // controllo se vietato dynData
                                                if (IOBConfFull.FluxLog.SendDynDataRec || item.Key != "DYNDATA")
                                                {
                                                    // verifico NON sia un ND...
                                                    if (item.Key == "ND" || string.IsNullOrEmpty(item.Key))
                                                    {
                                                        // log anomalia...
                                                        lgTrace($"Errore in processDynData.02: rawJob.key risulta ND! | rawJob.key: {item.Key} | rawJob.Value: {item.Value}");
                                                    }
                                                    else
                                                    {
                                                        sVal = string.Format("[DYNDATA]{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);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                        // salvo array...
                                        lastDynData = currDynData;
                                    }
                                    catch (Exception exc)
                                    {
                                        lgError(exc, "Eccezione in processDynData");
                                    }
                                }
                                // ora popolo prod data dai dynData...
                                foreach (var item in currDynData)
                                {
                                    // se configurato x scrivere valori in WRITE PROD
                                    if (IOBConfFull.FluxLog.CopyDyn2MemWrite)
                                    {
                                        // se presente nelle memorie write/read --> metto in curr data...
                                        if (memMap != null && memMap.mMapWrite.ContainsKey(item.Key) && !string.IsNullOrEmpty(item.Value))
                                        {
                                            upsertKey(item.Key, item.Value);
                                        }
                                    }
                                    // verifico area read x i lastProd...
                                    if (memMap != null && memMap.mMapRead.ContainsKey(item.Key) && !string.IsNullOrEmpty(item.Value))
                                    {
                                        upsertKeyLP(item.Key, item.Value);
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        lgError("Errore connessione mancante x getDynData");
                    }
                }
            }
        }

        /// <summary>
        /// Processa gestione memoria x invio dati QUANDO IOB è disconnesso da CNC...
        /// </summary>
        public void processMemoryDiscon()
        {
            // init obj display
            newDisplayData currDispData = new newDisplayData();
            // controllo contatore invio "keepalive"... invio solo a scadenza
            int disconnMaxSec = utils.CRI("disconMaxSec");
            if (DateTime.Now.Subtract(lastDisconnCheck).TotalSeconds > disconnMaxSec)
            {
                // resetto tutti i valori BYTE IN/PREV/OUT... così invio macchina spenta...
                B_input = 0;
                B_output = 0;
                B_previous = -1;
                accodaSigIN(ref currDispData);
                // update controllo
                lastDisconnCheck = DateTime.Now;
                lgInfo($"Send 00 | disconMaxSec: {disconnMaxSec}");
            }
            raiseRefresh(currDispData);
        }

        /// <summary>
        /// Effettua processing mode/status (EDIT/MDI/...)
        /// </summary>
        public virtual void processMode()
        { }

        /// <summary>
        /// Effettua processing ALTRI contatori/parametri (ed invia ad IO)
        /// </summary>
        public virtual async Task processOtherCounters()
        {
            await Task.Delay(1);
        }

        /// <summary>
        /// Effettua processing del recupero delle OVERRIDE (spindle, feedrate, rapid)
        /// </summary>
        public virtual void processOverride()
        {
            bool enableByApp = utils.CRB("enableOverrides");
            Dictionary<string, string> currOverride = new Dictionary<string, string>();
            if (enableByApp || IOBConfFull.FluxLog.EnableOverrides)
            {
                lgInfo("Inizio processOverride");
                if (connectionOk)
                {
                    currOverride = getOverrides();
                }
                else
                {
                    lgError("Errore connessione mancante x getOverrides");
                }

                // SE sono connesso...
                if (connectionOk)
                {
                    // se HO dei valori override...
                    if (currOverride.Count > 0)
                    {
                        // verifico SE sia cambiato il programma...
                        if (lastOverrideFS != currOverride["FEED_OVER"] || lastOverrideRapid != currOverride["RAPID_OVER"])
                        {
                            // salvo!
                            lastOverrideFS = currOverride["FEED_OVER"];
                            lastOverrideRapid = currOverride["RAPID_OVER"];
                            // per ogni valore del dizionario mostro ed accodo!
                            string sVal = "";
                            foreach (var item in currOverride)
                            {
                                // verifico NON sia un ND...
                                if (item.Key == "ND" || string.IsNullOrEmpty(item.Key))
                                {
                                    // log anomalia...
                                    lgTrace($"Errore in processOverride: rawJob.key risulta ND! | rawJob.key: {item.Key} | rawJob.Value: {item.Value}");
                                }
                                else
                                {
                                    sVal = string.Format("[OVERRIDES]{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>
        /// Processa esecuzione task ricevuti
        /// </summary>
        /// <param name="task2exe"></param>
        /// <param name="codTav"></param>
        /// <returns>Restituisce elenco task svolti --> per accodamento risposta</returns>
        public Dictionary<string, string> ProcessTask(Dictionary<string, string> task2exe, string codTav)
        {
            Dictionary<string, string> taskDone = new Dictionary<string, string>();
            Dictionary<string, string> task2Add = new Dictionary<string, string>();
            // eseguo realmente solo se NON disabilitata questa gestione (caso doppio PLC/HMI)...
            if (!IOBConfFull.Device.DisabExeTask)
            {
                if (task2exe != null && task2exe.Count > 0)
                {
                    string logMsg = $"Task2Exe S01: {task2exe.Count} task ricevuti";
                    if (!string.IsNullOrEmpty(codTav))
                    {
                        logMsg += $" | codTav: {codTav}";
                    }
                    lgInfo(logMsg);
                    int idTask = 0;
                    foreach (var item in task2exe)
                    {
                        idTask++;
                        lgInfo($"[{idTask:00}] - {item.Key} --> {item.Value}");
                        // verifico SE il task abbia un duplo writeLink e nel caso lo aggiungo...
                        var linkVal = getOptWriteLink(item.Key);
                        if (!string.IsNullOrEmpty(linkVal))
                        {
                            // aggiungo a task2add SE manca...
                            if (!task2Add.ContainsKey(linkVal))
                            {
                                task2Add.Add(linkVal, item.Value);
                            }
                            lgInfo($"Aggiunta task linked: {linkVal} -> {item.Value}");
                        }
                    }
                    // se ho task Link da aggiungere li aggiungo!
                    if (task2Add.Count > 0)
                    {
                        foreach (var item in task2Add)
                        {
                            task2exe.Add(item.Key, item.Value);
                        }
                    }
                    // chiamo procedura esecutiva (diversa x ogni IOB)
                    taskDone = executeTasks(task2exe, codTav);
                    lgInfo($"Task2Exe S02: eseguiti {taskDone.Count} task");
                    // loggo tutti i task done...
                    foreach (var item in taskDone)
                    {
                        sendToTaskWatch(item.Key, item.Value, codTav);
                    }
                }
            }
            return taskDone;
        }

#if false
        /// <summary>
        /// Processa esecuzione task ricevuti
        /// </summary>
        /// <param name="task2exe"></param>
        /// <param name="codTav"></param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> ProcessTask(Dictionary<string, string> task2exe, string codTav)
        {
            Dictionary<string, string> taskDone = new Dictionary<string, string>();
            Dictionary<string, string> task2Add = new Dictionary<string, string>();
            // eseguo realmente solo se NON disabilitata questa gestione (caso doppio PLC/HMI)...
            if (!IOBConfFull.Device.DisabExeTask)
            {
                if (task2exe != null)
                {
                    string logMsg = $"Task2Exe S01: {task2exe.Count} task ricevuti";
                    if (!string.IsNullOrEmpty(codTav))
                    {
                        logMsg += $" | codTav: {codTav}";
                    }
                    lgInfo(logMsg);
                    int idTask = 0;
                    foreach (var item in task2exe)
                    {
                        idTask++;
                        lgInfo($"[{idTask:00}] - {item.Key} --> {item.Value}");
                        // verifico SE il task abbia un duplo writeLink e nel caso lo aggiungo...
                        var linkVal = getOptWriteLink(item.Key);
                        if (!string.IsNullOrEmpty(linkVal))
                        {
                            // aggiungo a task2add SE manca...
                            if (!task2Add.ContainsKey(linkVal))
                            {
                                task2Add.Add(linkVal, item.Value);
                            }
                            lgInfo($"Aggiunta task linked: {linkVal} -> {item.Value}");
                        }
                    }
                    // se ho task Link da aggiungere li aggiungo!
                    if (task2Add.Count > 0)
                    {
                        foreach (var item in task2Add)
                        {
                            task2exe.Add(item.Key, item.Value);
                        }
                    }
                    // chiamo procedura esecutiva (diversa x ogni IOB)
                    taskDone = executeTasks(task2exe, codTav);
                    lgInfo($"Task2Exe S02: eseguiti {taskDone.Count} task");
                    // loggo tutti i task done...
                    foreach (var item in taskDone)
                    {
                        sendToTaskWatch(item.Key, item.Value, codTav);
                    }
                    // ora chiamo la cancellazione dei task eseguiti...
                    foreach (var item in taskDone)
                    {
                        await remTask2exe(item.Key, item.Value, codTav);
                    }
                }
            }
            return taskDone;
        }
#endif

        /// <summary>
        /// Classe fittizia in caso di processing task in MsVHF
        /// </summary>
        public virtual void processVHF()
        {
        }

        /// <summary>
        /// Classe fittizia in caso di processing watchdog data
        /// </summary>
        public virtual void processWhatchDog()
        {
        }

        /// <summary>
        /// Effettua rilettura del contapezzi dal server MP/IO
        /// </summary>
        /// <param name="forceCountRec">Forza rilettura da DB tempi ciclo rilevati</param>
        public void pzCntReload(bool forceCountRec, string forceMach = "")
        {
            // se NON disabilitato contapezzi
            if (!IOBConfFull.Device.EnabPzCount)
            {
                lgDebug("pzCntReload disabilitato da EnabPzCount");
            }
            else
            {
                // legge da IO server ULTIMO valore CONTPEZZI al riavvio...
                string currServerCount = "";
                string lastIdxODL = "";
                string calcUrl = "";
                if (!disableOdl)
                {
                    var srvAlive = CheckServerAlive();
                    if (srvAlive)
                    {
                        if (isMulti)
                        {
                            // disabilitato per macchina MULTI, da riportare logica da OpcUa ...
                        }
                        else
                        {
                            // leggo PRIMA ODL ....
                            calcUrl = string.IsNullOrEmpty(forceMach) ? urlGetCurrODL : urlGetCurrODL.Replace(IOBConfFull.General.CodIOB, forceMach);
                            lastIdxODL = utils.callUrl(calcUrl);
                            lgTrace($"Lettura ODL dall'url {calcUrl} --> {lastIdxODL}");
                            // se ho valori in coda da trasmettere uso dati REDIS
                            if (forceCountRec)
                            {
                                // uso dati da TCiclo registrati...
                                calcUrl = string.IsNullOrEmpty(forceMach) ? urlGetPzCountRec : urlGetPzCountRec.Replace(IOBConfFull.General.CodIOB, forceMach);
                                currServerCount = utils.callUrl(calcUrl);
                                lgInfo($"Lettura contapezzi da TCiclo registrati dall'url {calcUrl} --> num pz: {currServerCount}");
                            }
                            else
                            {
                                // uso il contapezzi dichiarato dall'IOB stesso
                                calcUrl = string.IsNullOrEmpty(forceMach) ? urlGetPzCount : urlGetPzCount.Replace(IOBConfFull.General.CodIOB, forceMach);
                                currServerCount = utils.callUrl(calcUrl);
                                lgInfo($"Lettura contapezzi dall'url {calcUrl} --> {currServerCount}");
                            }
                            // controllo: SE NON HO ODL...
                            if (string.IsNullOrEmpty(lastIdxODL) || lastIdxODL == "0")
                            {
                                // NON AGGIORNO
                                contapezziIOB = contapezziPLC;
                                lgError($"Errore lettura ODL (vuoto) resta tutto invariato contapezzi: {contapezziIOB} | contapezziPLC {contapezziPLC}");
                            }
                            else
                            {
                                if (!string.IsNullOrEmpty(currServerCount))
                                {
                                    // se "-1" resto a ultimo...
                                    if (currServerCount != "-1")
                                    {
                                        int newVal = -1;
                                        Int32.TryParse(currServerCount, out newVal);
                                        contapezziIOB = newVal > -1 ? newVal : contapezziIOB;
                                        lgInfo("Ricevuta conferma da server di {0} pezzi registrati per ODL", currServerCount);
                                    }
                                    else
                                    {
                                        // NON AGGIORNO
                                        contapezziIOB = contapezziPLC;
                                        lgError($"Errore lettura contapezzi (-1) - uso contapezziPLC --> {contapezziPLC}");
                                    }
                                }
                                else
                                {
                                    // registro che ho UN NUOVO ODL
                                    lgInfo($"Lettura ODL in pzCntReload, currIdxODL {currIdxODL} --> lastIdxODL {lastIdxODL}");
                                    // se ODL differente e NUOVO è zero --> resetto!
                                    if (currIdxODL.ToString() != lastIdxODL && lastIdxODL == "0")
                                    {
                                        // cambiato ODL quindi reset...
                                        contapezziIOB = 0;
                                        lgInfo("Nuovo ODL==0, RESET contapezzi (-->ZERO)");
                                    }
                                }
                                // provo a salvare nuovo ODL
                                int.TryParse(lastIdxODL, out currIdxODL);
                                lgInfo($"ODL | currIdxODL: {currIdxODL}");
                            }
                        }
                    }
                    else
                    {
                        // se server NON pronto...
                        contapezziIOB = contapezziPLC;
                        lgWarn("Errore server NON pronto in pzCntReload");
                    }
                }
            }
        }

        /// <summary>
        /// Fornisce il valore di flusso e valore in formato valido x messa in coda nel formato
        /// dtEve#flusso#valore#cont <paramref name="flusso">Flusso dati</paramref><paramref
        /// name="valore">Valore da salvare</paramref>
        /// </summary>
        public string qEncodeFLog(string flusso, string valore)
        {
            string answ = "";
            // solo se valore !="", su DynData...
            if (flusso != "DYNDATA" || !string.IsNullOrEmpty(valore))
            {
                try
                {
                    answ = $"{DateTime.Now:yyyyMMddHHmmssfff}#{flusso}#{valore}#{counterULog}";
                }
                catch
                { }
            }
            return answ;
        }

        /// <summary>
        /// Fornisce il valore di flusso e valore in formato valido x messa in coda nel formato
        /// dtEve#flux#valReq#cont <paramref name="eventDT">DataOra evento
        /// registrato</paramref><paramref name="flusso">Flusso dati</paramref><paramref
        /// name="valore">Valore da salvare</paramref>
        /// </summary>
        public string qEncodeFLog(DateTime eventDT, string flusso, string valore)
        {
            string answ = "";
            try
            {
                answ = $"{eventDT:yyyyMMddHHmmssfff}#{flusso}#{valore}#{counterFLog}";
            }
            catch
            { }
            return answ;
        }

        /// <summary>
        /// Fornisce il valore di UserLog e valore in formato valido x messa in coda nel formato:
        /// dtEve#flusso#valReq#cont#matrOpr#label#valNum <paramref name="flusso">Flusso dati
        /// (RC/RS/DI)</paramref><paramref name="valore">Valore da inviare
        /// (valString</paramref><paramref name="matrOpr">Matricola operatore</paramref><paramref
        /// name="label">Valore etichetta: causale scarto / tagCode</paramref><paramref
        /// name="valNum">Valore numerico: esitoOk (0/1) / nuo scarti</paramref>
        /// </summary>
        public string qEncodeULog(string flusso, string valore, int matrOpr, string label, int valNum)
        {
            string answ = "";
            try
            {
                answ = $"{DateTime.Now:yyyyMMddHHmmssfff}#{flusso}#{valore}#{matrOpr}#{label}#{valNum}#{counterULog}";
            }
            catch
            { }
            return answ;
        }

        /// <summary>
        /// Effettua lettura dati
        /// <paramref name="currDispData">Parametri da aggiornare x display in form</paramref>
        /// </summary>
        public virtual void readAllData(ref newDisplayData currDispData)
        {
            if (currDispData == null)
            {
                currDispData = new newDisplayData();
            }
            try
            {
                if (DemoIn)
                {
                    // segnalo che sono in Demo
                    currDispData.semIn = Semaforo.SV;
                }
                if (connectionOk)
                {
                    if (!IOBConfFull.Device.DisabStateCh)
                    {
                        readSemafori(ref currDispData);
                    }
                    else
                    {
                        // forzo lettura OK
                        currDispData.semIn = Semaforo.SV;
                    }
                }
                else
                {
                    lgError("Errore connessione mancante x readSemafori");
                }

                nReadIN++;
                // aggiorno valore mostrato...
                displayRawData(ref currDispData);
            }
            catch
            {
                currDispData.semIn = Semaforo.SR;
            }
            raiseRefresh(currDispData);
        }

        /// <summary>
        /// Effettua lettura semafori principale <paramref name="currDispData">Parametri da
        /// aggiornare x display in form</paramref>
        /// </summary>
        public virtual void readSemafori(ref newDisplayData currDispData)
        {
            lastReadPLC = DateTime.Now;
        }

        /// <summary>
        /// Aggiunge ai dati da inviare alla parentform i valori di RawInput rilevati (32bit)
        /// 2021.08.27: filtrati secondo i valori setup start/size RawDataInput
        /// </summary>
        public virtual void reportRawInput(ref newDisplayData currDispData)
        {
            // processo eventualmente aggiungendo ad elementi esistenti...
            if (currDispData == null)
            {
                currDispData = new newDisplayData();
            }
            try
            {
                StringBuilder sb = new StringBuilder();
                sb.Append($"B_input -->  {(short)B_input}{Environment.NewLine}");
                sb.Append($"{baseUtils.binaryForm(B_input)}{Environment.NewLine}");
                sb.Append($"{Environment.NewLine}");
                sb.Append($"----------- RAW Data BankConf -----------{Environment.NewLine}");
                // Do x scontato siano VALIDI i valori di RawDataInputStart / size --> filtro...
                var filtRawData = new byte[RawDataInputSize];
                Array.Copy(RawInput, RawDataInputStart, filtRawData, 0, RawDataInputSize);
                int i = RawDataInputStart;
                foreach (var item in filtRawData)
                {
                    sb.Append($"B{i:00} -->  {baseUtils.binaryForm(item)} = {(short)item}{Environment.NewLine}");
                    i++;
                }
                sb.Append("-------------------------------");
                currDispData.currBitmap = sb.ToString();
            }
            catch
            { }
        }

        /// <summary>
        /// Metodo generico di reset contapezzi...
        /// </summary>
        /// <returns></returns>
        public virtual bool resetContapezziPLC(string codTav)
        {
            lgInfo("Generic.resetContapezziPLC");
            return false;
        }

        /// <summary>
        /// Effettua salvataggio in LUT del valore ricevuto (valori string)
        /// - non serve calcolo medie o altro
        /// - accoda in out val e basta
        /// </summary>
        /// <param name="outVal">Array eventi da popolare</param>
        /// <param name="valore">valore da salvare</param>
        /// <param name="chiave">ID/chiave di riferimento</param>
        /// <returns></returns>
        public virtual void saveAlarmString(ref Dictionary<string, string> outVal, string valore, string chiave)
        {
            DateTime adesso = DateTime.Now;
            //check obj preliminare
            if (outVal == null)
            {
                outVal = new Dictionary<string, string>();
            }
            // default invio: blindato a 120 sec SE non trova conf x fluxLogResendPeriod
            int vetoSendAlarms = 120;
            if (fluxLogReduce)
            {
                vetoSendAlarms = 60 * fluxLogResendPeriod;
            }
            // verifico se sia scaduto OVVERO variato rispetto a prima oppure oltre tempo
            bool scaduto = !LastTSSSend.ContainsKey(chiave) || (LastTSSSend[chiave].AddSeconds(vetoSendAlarms) < adesso);
            bool cambiato = !LastTSS.ContainsKey(chiave) || !LastTSS[chiave].Equals(valore);
            if (scaduto || cambiato)
            {
                outVal.Add(chiave, valore);
                LastTSS[chiave] = valore;
                LastTSSSend[chiave] = adesso;
            }
        }

        /// <summary>
        /// metodo dummy x salvataggio aree memoria conf x CN
        /// </summary>
        /// <param name="tipo">tipo di DUMP</param>
        public virtual void saveMemDump(dumpType tipo)
        {
        }

        /// <summary>
        /// Effettua salvataggio in LUT del valore ricevuto (valori numerici)
        /// </summary>
        /// <param name="outVal"></param>
        /// <param name="valore"></param>
        /// <param name="chiave"></param>
        /// <returns></returns>
        public virtual void saveValue(ref Dictionary<string, string> outVal, string chiave, double valore)
        {
            //check obj preliminare
            if (outVal == null)
            {
                outVal = new Dictionary<string, string>();
            }
            bool scaduto = stackVal_TSVC(chiave, valore, DateTime.Now);
            // recupero VC
            valore = getVal_TSVC(chiave, scaduto);

            // 2025.08.05: verifico se il valore SIA configurato come onlyIncr...
            if (IOBConfFull.Memory.mMapRead.ContainsKey(chiave))
            {
                // in quel caso recupero ultimo valore
                var dataRec = IOBConfFull.Memory.mMapRead[chiave];
                // ....e se inferiore uso quello...
                if (dataRec.onlyIncr && valore < LastTSVC[chiave])
                {
                    valore = LastTSVC[chiave];
                }
            }

            // 2023.11.15: gestione deduplica (opzionale) fluxlog... in caso invalida scaduto...
            if (fluxLogReduce && scaduto)
            {
                /*-------------------------------
                 * logica processing:
                 * - confronto con valore precedente (se non c'è allora registro questo)
                 * - se variato OLTRE deadband --> nulla cambia
                 * - se NON variato x deadband --> accodo SOLO SE scaduto tempo resend
                 * ------------------------------ */
                DateTime adesso = DateTime.Now;
                // cerco tra i veto...
                if (fluxLogReduceVeto.ContainsKey(chiave))
                {
                    // per prima cosa verifico che sia ancora valido il veto...
                    if (adesso < fluxLogReduceVeto[chiave])
                    {
                        // verifico valore precedente + deadband x decidere se sia davvero scaduto
                        if (fluxLogReduceLast.ContainsKey(chiave))
                        {
                            scaduto = Math.Abs(fluxLogReduceLast[chiave] - valore) > fluxLogRedDeadBand;
                            if (scaduto)
                            {
                                lgTrace($"saveValue: deadBand superata | {chiave} | old: {fluxLogReduceLast[chiave]:F3} | {valore:F3} | DBand: {fluxLogRedDeadBand}");
                            }
                        }
                    }
                    // altrimenti creo un nuovo veto lasciando inalterata la scadenza...
                    else
                    {
                        fluxLogReduceVeto[chiave] = adesso.AddMinutes(fluxLogResendPeriod);
                    }
                }
                // se non ci fosse metto veto con periodo std...
                else
                {
                    fluxLogReduceVeto.Add(chiave, adesso.AddMinutes(fluxLogResendPeriod));
                    if (fluxLogReduceLast.ContainsKey(chiave))
                    {
                        fluxLogReduceLast[chiave] = valore;
                    }
                    else
                    {
                        fluxLogReduceLast.Add(chiave, valore);
                    }
                }
            }
            if (scaduto)
            {
                /*--------------------------------------------------
                 * FIX gestione decimali e formati IT/EN
                 * - limite a 3 digit x valore float
                 * - culture invariant (
                 * - richiede CHECK i vari double/float parser...
                 *
                 * per decodificare usare ad esempio:
                 *
                 * bool success = double.TryParse(rawVal, NumberStyles.Any, CultureInfo.InvariantCulture, out cntDouble);
                 * */
                outVal.Add(chiave, valore.ToString("F3", CultureInfo.InvariantCulture));
                //outVal.Add(chiave, $"{valore:N3}");
                LastTSVC[chiave] = valore;
                fluxLogReduceLast[chiave] = valore;
                lgDebug($"saveValue: valore accodato | {chiave}: {valore:F3}");
            }
            else
            {
                lgTrace($"saveValue: filtro attivo | {chiave}: {valore:F3}");
            }
        }

        /// <summary>
        /// Effettua salvataggio in LUT del valore ricevuto (valori string)
        /// - non serve calcolo medie o altro
        /// - accoda in out val e basta
        /// </summary>
        /// <param name="outVal">Array eventi da popolare</param>
        /// <param name="chiave">ID/chiave di riferimento</param>
        /// <param name="valore">valore da salvare</param>
        /// <returns></returns>
        public virtual void saveValueString(ref Dictionary<string, string> outVal, string chiave, string valore)
        {
            DateTime adesso = DateTime.Now;
            //check obj preliminare
            if (outVal == null)
            {
                outVal = new Dictionary<string, string>();
            }
            int maxVetoSeconds = 60;

            // cerco VC...
            if (TSVC_Data.ContainsKey(chiave))
            {
                maxVetoSeconds = TSVC_Data[chiave].Period;
            }
            // se ho attivo il veto invio fluxLogReduce metto periodo a minuti indicati...
            if (fluxLogReduce)
            {
                maxVetoSeconds = fluxLogResendPeriod * 60;
            }

            // verifico se sia scaduto OVVERO variato rispetto a prima oppure oltre tempo
            bool scaduto = !LastTSSSend.ContainsKey(chiave) || (LastTSSSend[chiave].AddSeconds(maxVetoSeconds) < adesso);
            bool cambiato = !LastTSS.ContainsKey(chiave) || !LastTSS[chiave].Equals(valore);
            if (scaduto || cambiato)
            {
                outVal.Add(chiave, valore);
                LastTSS[chiave] = valore;
                LastTSSSend[chiave] = adesso;
            }
        }

        /// <summary>
        /// Invio la variazione dello stato allarmi (se avvenuta)
        /// </summary>
        /// <param name="memAddr">COD memoria allarmi</param>
        /// <param name="index">Indice memoria allarmi</param>
        /// <param name="lastStatus">ultimo stato rilevato</param>
        /// <param name="currStatus">stato corrente</param>
        /// <param name="AlarmList">Lista allarmi validi per l'area</param>
        /// <returns></returns>
        public bool sendAlarmVariations(string memAddr, int index, uint lastStatus, uint currStatus, List<string> AlarmList)
        {
            bool fatto = false;
            if (lastStatus != currStatus)
            {
                List<string> ActiveAlarmList = new List<string>();
                // invio GET del MemoryAddress, del banco e del valore currStatus
                lastUrl = $"{urlSendAlarm}?memAddr={memAddr}&index={index}&currStatus={currStatus}";
                if (currStatus == 0)
                {
                    ActiveAlarmList.Add("ALL OK");
                }
                else
                {
                    // calcolo quali allarmi mostrare dato currStatus ed elenco allarmi
                    string bitMap = Convert.ToString(currStatus, 2);
                    int bitLen = bitMap.Length;
                    // ciclo x associare tutti gli allarmi
                    for (int i = 0; i < bitLen; i++)
                    {
                        // se è 1 --> aggiungo!
                        if (bitMap[bitLen - i - 1] == '1')
                        {
                            // se sono entro limiti
                            if (AlarmList.Count >= i)
                            {
                                ActiveAlarmList.Add($"{i:000}-{AlarmList.ElementAt(i)}");
                            }
                            //else
                            //{
                            //    ActiveAlarmList.Add($"Unknown Num.{i}");
                            //}
                        }
                    }
                }

                string rawData = JsonConvert.SerializeObject(ActiveAlarmList);
                string resp = utils.CallUrlPost(lastUrl, rawData);
                //string resp = utils.callUrlAsync(lastUrl, rawData);
                if (resp != null)
                {
                    if (resp.ToUpper() == "OK")
                    {
                        // segnalo okReport
                        fatto = true;
                    }
                }
                else
                {
                    lgError($"Errore in invio richiesta registrazione allarme | resp: {resp} | URL: {lastUrl}{Environment.NewLine}Payload:{Environment.NewLine}{rawData}");
                }
            }
            return fatto;
        }

        /// <summary>
        /// Invia una LISTA di valori
        /// </summary>
        /// <param name="tipoUrl"></param>
        /// <param name="queueVal"></param>
        public async Task<bool> sendDataBlock(urlType tipoUrl, List<string> listQueueVal, bool force = false)
        {
            bool fatto = false;
            // init obj display
            newDisplayData currDispData = new newDisplayData();
            if (listQueueVal != null)
            {
                try
                {
                    // recupero e formatto URL dati da coda...
                    lastUrl = urlDataBlock(tipoUrl);
                    // in base al tipo di dato compongo il payload Json da inviare
                    string payload = jsonPayload(tipoUrl, listQueueVal);
                    // async a true SE FLog
                    bool doAsync = tipoUrl == urlType.FLog ? true : false;
                    // se NON sono in demo effettuo invio!
                    if (!DemoOut)
                    {
                        // SE server alive...
                        if (await CheckServerAliveAsync())
                        {
                            // chiamo URL!
                            string answ = await callUrlWithPayloadAsync(lastUrl, payload, doAsync);

                            // valutare invio REST alternativo...
#if false
                            // invio alternativo nuovo
                            if (string.IsNullOrEmpty(answ))
                            {
                                answ = utils.ExecCallPostPlain(lastUrl, payload);
                            }
#endif

                            // loggo!
                            lgInfo($"[SEND payload] TipoURL: {tipoUrl} | {listQueueVal.Count} records --> {answ}");
                            // se "OK" verde, altrimenti errore --> ROSSO
                            if (answ.Contains("OK"))
                            {
                                fatto = true;
                                currDispData.semOut = Semaforo.SV;
                                // se oltre 1 min NON era online --> check pezzi!
                                if (DateTime.Now.Subtract(lastIobOnline).TotalMinutes > 1 && !isMulti)
                                {
                                    lgInfo($"sendDataBlock --> offline timeout ({lastIobOnline}) --> pzCntReload(true)");
                                    pzCntReload(true);
                                }
                                lastIobOnline = DateTime.Now;
                            }
                            else
                            {
                                currDispData.semOut = Semaforo.SR;
                            }
                        }
                        else
                        {
                            lgInfo($"[SERVER KO] {listQueueVal.Count}");
                        }
                    }
                    else
                    {
                        currDispData.semOut = Semaforo.SV;
                        // loggo!
                        lgInfo($"{listQueueVal.Count} records --> [SIM]");
                    }
                    nSendOut += listQueueVal.Count;
                    // riporto cosa inviato
                    currDispData.newUrlCallData = lastUrl;
                    // aggiorno data ultimo watchdog...
                    lastWatchDog = DateTime.Now;
                }
                catch
                {
                    currDispData.semOut = Semaforo.SR;
                }
            }
            raiseRefresh(currDispData);
            return fatto;
        }

        /// <summary>
        /// Effettua invio a MoonPro del valore richiesto
        /// </summary>
        /// <param name="tipoUrl"></param>
        /// <param name="queueVal">
        /// Valore da trasmettere: es
        /// INPUT: lo status rilevato in HEX
        /// FLog: il valore da trasmettere per il flusso indicato
        /// </param>
        public async Task sendToMoonPro(urlType tipoUrl, string queueVal)
        {
            // controllo NON nullo..
            if (queueVal != null)
            {
                // init obj display
                newDisplayData currDispData = new newDisplayData();
                try
                {
                    // recupero e formatto URL dati da coda...
                    switch (tipoUrl)
                    {
                        case urlType.FLog:
                            lastUrl = urlFLog(queueVal);
                            break;

                        case urlType.SignIN:
                            lastUrl = urlInput(queueVal);
                            break;

                        default:
                            lastUrl = "";
                            break;
                    }
                    // se NON sono in demo effettuo invio!
                    if (!DemoOut)
                    {
                        // SE server alive...
                        if (await CheckServerAliveAsync())
                        {
                            // chiamo URL!
                            string answ = await callUrl(lastUrl, false);
                            // loggo!
                            lgDebug(string.Format("[SEND] {0} -> {1}", queueVal, answ));
                            // se oltre 1 min NON era online --> check pezzi!
                            if (DateTime.Now.Subtract(lastIobOnline).TotalMinutes > 1 && !isMulti)
                            {
                                lgInfo($"sendToMoonPro --> offline timeout ({lastIobOnline}) --> pzCntReload(true)");
                                pzCntReload(true);
                            }
                            // se richiesto effettuo refresh contapezzi
                            if (needRefreshPzCount && !isMulti)
                            {
                                pzCntReload(true);
                                needRefreshPzCount = false;
                            }
                            lastIobOnline = DateTime.Now;
                            // se "OK" verde, altrimenti errore --> ROSSO
                            if (answ == "OK")
                            {
                                currDispData.semOut = Semaforo.SV;
                            }
                            else
                            {
                                currDispData.semOut = Semaforo.SR;
                            }
                        }
                        else
                        {
                            lgError(string.Format("[SERVER KO] {0}", queueVal));
                        }
                    }
                    else
                    {
                        currDispData.semOut = Semaforo.SV;
                        // loggo!
                        lgDebug(string.Format("{0} -> [SIM]", queueVal));
                    }
                    nSendOut++;
                    currDispData.newUrlCallData = lastUrl;
                    // aggiorno data ultimo watchdog...
                    lastWatchDog = DateTime.Now;
                }
                catch
                {
                    currDispData.semOut = Semaforo.SR;
                }
                raiseRefresh(currDispData);
            }
            else
            {
                lgTrace($"Richiesto invio valore nullo, salto");
            }
        }

        /// <summary>
        /// Metodo generico di IMPOSTAZIONE FORZATA del contapezzi...
        /// </summary>
        /// <param name="newPzCount">Pezzi richiesti</param>
        /// <returns></returns>
        public virtual bool setcontapezziPLC(int newPzCount, string codTav)
        {
            return false;
        }

        /// <summary>
        /// Effettua impostazione del conteggio pezzi richiesti
        /// </summary>
        /// <returns></returns>
        public virtual bool setPzComm(int pzReq)
        {
            return false;
        }

        /// <summary>
        /// Processing di Variabili Campionarie x TimeSeries, accoda valori x la VC (se esiste) e
        /// restituisce bool val se SCADUTO periodo controllo
        /// </summary>
        /// <param name="VCName">Nome della VC</param>
        /// <param name="VCVal">Valore (nuovo) delal VC</param>
        /// <returns></returns>
        public bool stackVal_TSVC(string VCName, double VCVal, DateTime dtLimit)
        {
            bool answ = false;
            // cerco VC...
            if (TSVC_Data.ContainsKey(VCName))
            {
                TSVC_Data[VCName].dataArray.Add(VCVal);
                // ora verifico scadenza...
                if (TSVC_Data[VCName].DTStart.AddSeconds(TSVC_Data[VCName].Period) < dtLimit)
                {
                    // 2026.01.20: solo se ho almeno 3 valori altrimenti rischio zeri...
                    answ = TSVC_Data[VCName].dataArray.Count > 2;
                    //answ = true;
                }
                lgTrace($"stackVal_TSVC | {VCName} | {VCVal} | scaduta: {answ}");
            }
            return answ;
        }

        /// <summary>
        /// Avvia l'adapter sulla porta richiesta
        /// </summary>
        /// <param name="resetQueue">indica se sia richiesto di SVUOTARE le code delle info</param>
        public virtual void startAdapter(bool resetQueue)
        {
            DateTime adesso = DateTime.Now;
            DateTime scaduto = adesso.AddMinutes(-10);
            lgInfo("Starting adapter...");
            maxJsonData = utils.CRI("maxJsonData");
            maxJsonDataEv = utils.CRI("maxJsonDataEv");
            parentForm.commPlcActive = false;
            adpRunning = true;
            dtAvvioAdp = adesso;
            lastWatchDog = scaduto;
            lastPING = scaduto;
            lastReadPLC = scaduto;
            lastDisconnCheck = scaduto;
            TimingData.resetData();
            // aggiungo altri defaults
            setDefaults(resetQueue);
            adpTryRestart = true;
            // sistemo altri check avvio
            dtVetoQueueIN = DateTime.Now.AddSeconds(vetoQueueIn);
            queueInEnabCurr = false;
            string msgVeto = $"Impostato veto QUEUE-IN a {vetoQueueIn}s | fino alle {dtVetoQueueIN:yyyy.MM.dd HH:mm:ss}";
            lgInfoStartup(msgVeto);
            lgDebug($"startAdapter completed | veto queueIn impostato per {vetoQueueIn}s fino alle {dtVetoQueueIN:yyyy.MM.dd HH:mm:ss}");
        }

        /// <summary>
        /// Ferma l'adapter...
        /// </summary>
        /// <param name="tryRestart">
        /// indica se si debba tentare di riavviare l'adapter (con caduta connessione viene fermato
        /// in automatico)
        /// </param>
        /// <param name="forceDequeue">indica se sia richiesto di SVUOTARE le code delle info</param>
        public virtual async Task stopAdapter(bool tryRestart, bool forceDequeue)
        {
            // controllo che non ci sia redis queue altrimenti NON forza svuotamento...
            if (forceDequeue && !IOBConfFull.General.EnabRedisQue)
            {
                // svuoto le code dei valori letti e non ancora trasmessi...
                parentForm.displayTaskAndLog("[CLOSING] Svuotamento FORZATO coda segnali...", true);
                while (QueueIN.Count > 0)
                {
                    // INVIO COMUNQUE...!!!
                    string valore = "";
                    QueueIN.TryDequeue(out valore);
                    await sendToMoonPro(urlType.SignIN, valore);
                }
                parentForm.displayTaskAndLog("[CLOSING] Svuotamento FORZATO coda FluxLOG...", true);
                // se ho + di 2 elementi in coda --> uso invio JSON in blocco...
                if (QueueFLog.Count > minJsonData)
                {
                    while (QueueFLog.Count > 0)
                    {
                        List<string> listaValori = new List<string>();
                        // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                        if (QueueFLog.Count > maxJsonData)
                        {
                            string currVal = "";
                            // prendoi primi maxJsonDataValori
                            for (int i = 0; i < maxJsonData; i++)
                            {
                                QueueFLog.TryDequeue(out currVal);
                                listaValori.Add(currVal);
                            }
                            await sendDataBlock(urlType.FLog, listaValori);
                        }
                        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);
                        }
                    }
                    // HO FINITO invio di FLog...
                }
                else
                {
                    string currVal = "";
                    while (QueueFLog.Count > 0)
                    {
                        // INVIO COMUNQUE...!!!
                        QueueFLog.TryDequeue(out currVal);
                        await sendToMoonPro(urlType.FLog, currVal);
                    }
                }

                // svuoto coda ULog
                while (QueueULog.Count > 0)
                {
                    List<string> listaValori = new List<string>();
                    // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                    if (QueueULog.Count > maxJsonData)
                    {
                        string currVal = "";
                        // prendoi primi maxJsonDataValori
                        for (int i = 0; i < maxJsonData; i++)
                        {
                            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);
                    }
                }
            }
            parentForm.displayTaskAndLog("[STOP] Stopping adapter...", true);
            adpTryRestart = false;

            parentForm.displayTaskAndLog("[STOP] Stopping adapter - last periodic data read...", true);

            // salvo statistiche
            string callKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CallStats");
            await CallMetricsCollector.SaveToRedisAsync(redisMan.currDb, callKey, false);

            // chiudo la connessione all'adapter...
            tryDisconnect();
            dtStopAdp = DateTime.Now;
            adpTryRestart = tryRestart;
            adpRunning = false;
            // chiudo!
            parentForm.displayTaskAndLog("Adapter Stopped.", true);
            DateTime adesso = DateTime.Now;
            lastReadPLC = adesso;
            lastPING = adesso;
            parentForm.commPlcActive = false;
        }

        /// <summary>
        /// Processo la coda SignalIN...
        /// </summary>
        public async Task svuotaCodaSignInAsync()
        {
            // verifico SE la coda abbia dei valori...
            if (QueueIN.Count > 0)
            {
                // invio pacchetto di dati (max da conf)
                for (int i = 0; i < nMaxSend; i++)
                {
                    if (QueueIN.Count > 0)
                    {
                        string currVal = "";
                        // se online provo
                        if (MPOnline)
                        {
                            if (IobOnline)
                            {
                                // se ho + di 2 elementi in coda --> uso invio JSON in blocco...
                                if (QueueIN.Count > 1)
                                {
                                    List<string> listaValori = new List<string>();
                                    // se ho + di maxJsonData elementi --> invio un set di dati alla volta
                                    if (QueueIN.Count > maxJsonDataEv)
                                    {
                                        // prendoi primi maxJsonDataValori
                                        for (int j = 0; j < maxJsonDataEv; j++)
                                        {
                                            QueueIN.TryDequeue(out currVal);
                                            listaValori.Add(currVal);
                                        }
                                        await sendDataBlock(urlType.SignIN, listaValori);
                                    }
                                    else
                                    {
                                        // invio in blocco
                                        listaValori = QueueIN.ToList();
                                        // invio
                                        await sendDataBlock(urlType.SignIN, listaValori);
                                        // svuoto!
                                        QueueIN = new DataQueue(IOBConfFull.General.FilenameIOB, "QueueIN", IOBConfFull.General.EnabRedisQue, redisMan);
                                    }
                                }
                                else
                                {
                                    // INVIO SINGOLO...!!!
                                    QueueIN.TryDequeue(out currVal);
                                    await sendToMoonPro(urlType.SignIN, currVal);
                                }
                                // salvo come last signal in il currVal ultimo... SE !=""
                                if (!string.IsNullOrEmpty(currVal))
                                {
                                    lastSignInVal = currVal;
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Metodo base connessione...
        /// </summary>
        public virtual void tryConnect()
        {
            dtAvvioAdp = DateTime.Now;
            queueInEnabCurr = true;
        }

        /// <summary>
        /// Metodo base disconnessione...
        /// </summary>
        public virtual void tryDisconnect()
        {
            queueInEnabCurr = false;
        }

        /// <summary>
        /// Inserimento/aggiornamento chiavi/valore in currProdData + cache REDIS
        /// </summary>
        /// <param name="chiave"></param>
        /// <param name="valore"></param>
        /// <returns>True se modificato/inserito, false se INVARIATO</returns>
        public bool upsertKey(string chiave, string valore)
        {
            bool done = false;
            if (currProdData.ContainsKey(chiave))
            {
                // se variato inserisco...
                if (currProdData[chiave] != valore)
                {
                    currProdData[chiave] = valore;
                    done = true;
                }
            }
            else
            {
                currProdData.Add(chiave, valore);
                done = true;
            }
            if (done)
            {
                // salvo in redis...
                redisMan.redSaveHashDict(rKeyCurrProdData, currProdData);
            }
            return done;
        }

        /// <summary>
        /// Inserimento/aggiornamento chiavi/valore in lastProdData
        /// </summary>
        /// <param name="chiave"></param>
        /// <param name="valore"></param>
        /// <returns>True se modificato/inserito, false se INVARIATO</returns>
        public bool upsertKeyLP(string chiave, string valore)
        {
            bool done = false;
            if (lastProdData.ContainsKey(chiave))
            {
                // se variato inserisco...
                if (lastProdData[chiave] != valore)
                {
                    lastProdData[chiave] = valore;
                    done = true;
                }
            }
            else
            {
                lastProdData.Add(chiave, valore);
                done = true;
            }
            return done;
        }

        /// <summary>
        /// Formatta URL x invio in DataBlock Json dei dati FLog / eventi
        /// </summary>
        /// <param name="tipoUrl"></param>
        /// <returns></returns>
        public virtual string urlDataBlock(urlType tipoUrl)
        {
            // verifico la parte di link "tipoComando"
            string tipoComando = "";
            switch (tipoUrl)
            {
                case urlType.FLog:
                    tipoComando = "flogJson";
                    break;

                case urlType.SignIN:
                    tipoComando = "evListJson";
                    break;

                case urlType.RawTransf:
                    tipoComando = "rawTransfJson";
                    break;

                case urlType.ULog:
                    tipoComando = "ulogJson";
                    break;

                default:
                    break;
            }
            // URL base x input
            string answ = $@"{urlCommandIob(tipoComando)}";
            // se è disabilitato keepalive aggiungo opzione
            if (IOBConfFull.MapoMes.DisabKeepAlive)
            {
                // se c'è già "?" aggiungo con "&" altrimenti
                string sPar = answ.Contains("?") ? "&&" : "?";
                answ += $@"{sPar}disabKA=true";
            }
            return answ;
        }

        /// <summary>
        /// Fornisce URL di tipo FluxLog
        /// </summary>
        /// <param name="queueVal">valore salvato in coda nel formato dtEve#flux#valore#counter</param>
        /// <returns></returns>
        public string urlFLog(string queueVal)
        {
            // URL base x input
            string answ = $@"{urlCommandIob("flog")}";
            // decodifica valore!
            string[] valori = qDecodeIN(queueVal);
            // aggiungo flux e valore...
            answ += $@"?flux={valori[1]}&&valore={valori[2]}";
            // aggiondo dataOra evento e corrente + contatore...
            answ += $@"&&dtEve={valori[0]}&&dtCurr={DateTime.Now:yyyyMMddHHmmssfff}&&cnt={valori[3]}";
            // se è disabilitato keepalive aggiungo opzione
            if (IOBConfFull.MapoMes.DisabKeepAlive)
            {
                ;
                answ += $"&&disabKA=true";
            }
            return answ;
        }

        /// <summary>
        /// URL per recupero dati ODL alla data...
        /// </summary>
        public string urlGetOdlAtDate(DateTime dtRif)
        {
            string answ = $@"{urlCommandIob("getOdlAtDate")}?dateRif={dtRif:yyyyMMdd}";
            return answ;
        }

        /// <summary>
        /// Fornisce URL INPUT per i parametri richiesti
        /// </summary>
        /// <param name="queueVal">valore salvato in coda formato dtEve#valore#counter</param>
        /// <returns></returns>
        public string urlInput(string queueVal)
        {
            // URL base x input
            string answ = $@"{urlCommandIob("input")}";
            // decodifica valore!
            string[] valori = qDecodeIN(queueVal);
            // aggiungo macchina e valore...
            answ += $@"?valore={valori[1]}";
            // aggiondo dataOra evento e corrente + contatore...
            answ += $@"&&dtEve={valori[0]}&&dtCurr={DateTime.Now:yyyyMMddHHmmssfff}&&cnt={valori[2]}";
            return answ;
        }

        /// <summary>
        /// Fornisce URL di tipo UserLog
        /// </summary>
        /// <param name="queueVal">valore salvato in coda nel formato dtEve#flux#valore#counter</param>
        /// <returns></returns>
        public string urlULog(string queueVal)
        {
            // URL base x input
            string answ = $@"{urlCommandIob("ulog")}";
            // decodifica valore!
            string[] valori = qDecodeIN(queueVal);
            // aggiungo macchina e valore...
            answ += $@"?flux={valori[1]}&&valore={valori[2]}";
            // aggiondo dataOra evento e corrente + contatore...
            answ += $@"&&dtEve={valori[0]}&&dtCurr={DateTime.Now:yyyyMMddHHmmssfff}&&cnt={valori[3]}";
            return answ;
        }

        #endregion Public Methods

        #region Private Methods

        /// <summary>
        /// Test ping + api al server in modalità Async
        /// </summary>
        /// <param name="maxRetries"></param>
        /// <returns></returns>
        private async Task<bool> ExecuteApiCheckWithRetryAsync(int maxRetries)
        {
            var rand = new Random();
            for (int i = 0; i <= maxRetries; i++)
            {
                try
                {
                    // Se non è il primo tentativo, resetta i client e aspetta
                    if (i > 0)
                    {
                        if (i == 4) resetWebClients(); // Reset specifico a metà tentativi
                        await Task.Delay(rand.Next(150, 500));
                    }

                    string resp = await callUrl(urlAlive, false);
                    if (resp == "OK") return true;
                }
                catch (Exception ex)
                {
                    if (i == 0) lgError($"Errore API Check: {ex.Message}");
                }
            }
            return false;
        }

#if false
        /// <summary>
        /// Processing di una risposta raw di task2exe
        /// </summary>
        /// <param name="resp">Risposta come string RAW</param>
        /// <param name="codTav">Cod Tav (opzionale)</param>
        private async Task ProcessResp(string resp, string codTav)
        {
            Dictionary<string, string> task2exe = new Dictionary<string, string>();
            Dictionary<string, string> taskDone = new Dictionary<string, string>();
            if (!string.IsNullOrEmpty(resp) && resp.Length > 2)
            {
                try
                {
                    task2exe = JsonConvert.DeserializeObject<Dictionary<string, string>>(resp);
                    // se ho da fare chiamo esecuzione..
                    if (task2exe.Count > 0)
                    {
                        taskDone = await ProcessTask(task2exe, codTav);
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in ServerGetRequestsAsync.ProcessResp:{Environment.NewLine}{exc}");
                }
            }
        }
#endif

        /// <summary>
        /// Update stato server
        /// </summary>
        /// <param name="currentAlive"></param>
        private void UpdateServerState(bool currentAlive)
        {
            if (MPOnline != currentAlive)
            {
                MPOnline = currentAlive;
                parentForm.commSrvActive = currentAlive ? 1 : 0;

                if (currentAlive)
                {
                    lgInfo("SERVER ONLINE");
                    dtVetoPing = DateTime.Now.AddMilliseconds(baseUtils.nextPauseSendMSec * 10);
                }
                else
                {
                    lgError("SERVER OFFLINE");
                    // Veto standard per server offline
                    dtVetoPing = DateTime.Now.AddMilliseconds(baseUtils.nextPauseSendMSec * 20);
                    utils.dtVetoSend = dtVetoPing;
                }
            }
            else
            {
                // Se lo stato è invariato (es. era online e resta online), allunghiamo il prossimo controllo
                dtVetoPing = DateTime.Now.AddMilliseconds(baseUtils.nextPauseSendMSec * 30);
            }
        }

        #endregion Private Methods
    }
}