using IOB_UT_NEXT;
using IOB_UT_NEXT.Config;
using IOB_UT_NEXT.Config.Base;
using MapoSDK;
using Newtonsoft.Json;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace IOB_WIN_FORM
{
    public partial class AdapterForm : Form
    {
        #region Public Fields

        /// <summary>
        /// oggetto logging
        /// </summary>
        public static Logger lg;

        /// <summary>
        /// Configurazione gerarchica completa (v 4.x.x.x)
        /// </summary>
        public IobConfTree IOBConfFull;

        /// <summary>
        /// Oggetto x gestione dell'adapter GENERICO (x poter usare metodi di ognuno...)
        /// </summary>
        public Iob.Generic iobObj;

        #endregion Public Fields

        #region Public Constructors

        /// <summary>
        /// Avvio MainForm
        /// </summary>
        /// <param name="codIOB"></param>
        public AdapterForm(string codIOB)
        {
            CurrIOB = codIOB;
            // continuo avvio...
            InitializeComponent();
            myGraphInitForm();

            checkEditMes2Plc();

            // inizializzo orologi
            firstStart = DateTime.Now;
            lastStartTry = DateTime.Now.AddHours(-1);

            initDatamonitor();

            waitRecMSec = utils.CRI("waitRecMSec");

            LogManager.ReconfigExistingLoggers();

            lg = LogManager.GetCurrentClassLogger();
            displayTaskAndLog("AdapterForm Starting");
        }

        #endregion Public Constructors

        #region Public Properties

        public long alQueueLen
        {
            set
            {
                qAlLen = value;
                lblQueueAlarmLen.Text = qAlLen.ToString();
                showQueueData();
                // se supero max precedente, ed è > 10... loggo!
                if (qAlLen > maxAlQueue && qAlLen > 10)
                {
                    maxAlQueue = qAlLen;
                    lgInfo($"[WARN] Coda FLog di {value} record");
                }
                else
                {
                    maxAlQueue--;
                    maxAlQueue = maxAlQueue < qAlLen ? qAlLen : maxAlQueue;
                }
            }
            get
            {
                return qAlLen;
            }
        }

        /// <summary>
        /// Button restart encapsulato x gestione da classi derivate
        /// </summary>
        public Button btnRestart
        {
            get => restart;
        }

        /// <summary>
        /// Button start encapsulato x gestione da classi derivate
        /// </summary>
        public Button btnStart
        {
            get => start;
        }

        /// <summary>
        /// Button stop encapsulato x gestione da classi derivate
        /// </summary>
        public Button btnStop
        {
            get => stop;
        }

        /// <summary>
        /// Visualizzazione stato di comunicazione attiva con PLC
        /// </summary>
        public bool commPlcActive
        {
            get
            {
                return _commPlcActive;
            }
            set
            {
                if (_commPlcActive != value)
                {
                    _commPlcActive = value;

                    // Se l'applicazione è minimizzata, NON invocare la UI: risparmio il Context Switch del thread grafico.
                    if (!ShouldUpdateUI()) return;

                    // se true --> comunica/verde, altrimenti grigio
                    this.UIThread(delegate
                    {
                        lblCNC.ForeColor = value ? Color.SeaGreen : Color.Black;
                        //statusStrip1.Refresh();
                        statusStrip1.Invalidate();
                    });
                }
            }
        }

        /// <summary>
        /// Visualizzazione stato di comunicazione attiva con PLC: 0 = NO PING 1 = PING OK 2 = IOB enabled
        /// </summary>
        public int commSrvActive
        {
            get
            {
                return _commSrvActive;
            }
            set
            {
                _commSrvActive = value;

                if (!ShouldUpdateUI()) return;

                this.UIThread(delegate
                {
                    switch (value)
                    {
                        case 0:
                            lblSrvUrl.ForeColor = Color.Red;
                            break;

                        case 1:
                            lblSrvUrl.ForeColor = Color.OrangeRed;
                            break;

                        case 2:
                            lblSrvUrl.ForeColor = Color.SeaGreen;
                            break;

                        default:
                            break;
                    }
                    statusStrip1.Refresh();
                });
            }
        }

        public string counterIob
        {
            get
            {
                return lblPzCountIob.Text;
            }
            set
            {
                lblPzCountIob.Text = value;
            }
        }

        public string counterMac
        {
            get
            {
                return lblPzCountMac.Text;
            }
            set
            {
                lblPzCountMac.Text = value;
            }
        }

        /// <summary>
        /// Modello macchina
        /// </summary>
        public string curModel
        {
            get => IOBConfFull != null ? IOBConfFull.Device.Model : "";
        }

        /// <summary>
        /// Vendor macchina
        /// </summary>
        public string curVendor
        {
            get => IOBConfFull != null ? IOBConfFull.Device.Vendor : "";
        }

        /// <summary>
        /// Stringa dati monitoraggio mostrata (1 SX)...
        /// </summary>
        public string dataMonitor_0
        {
            set
            {
                DateTime adesso = DateTime.Now;
                // salvo in cima alla lista...
                dMonValues[0].Insert(0, value);
                // se supero limite --> trim!
                if (dMonValues[0].Count > nLine2show)
                {
                    dMonValues[0] = dMonValues[0].Take(nLine2show).ToList();
                }
                if (dMonDisplVetoVeto[0] < adesso)
                {
                    // update veto!
                    dMonDisplVetoVeto[0] = adesso.AddMilliseconds(delayShowLogMs);

                    if (!ShouldUpdateUI()) return;
                    // se scaduto veto --> display!
                    lblRawData.Text = String.Join(Environment.NewLine, dMonValues[0]);
                }
            }
        }

        /// <summary>
        /// Stringa dati monitoraggio mostrata (1 SX)...
        /// </summary>
        public string dataMonitor_1
        {
            set
            {
                DateTime adesso = DateTime.Now;
                // salvo nell'array...
                dMonValues[1].Insert(0, value);
                // se supero limite --> trim!
                if (dMonValues[1].Count > nLine2show)
                {
                    dMonValues[1] = dMonValues[1].Take(nLine2show).ToList();
                }
                if (dMonDisplVetoVeto[1] < adesso)
                {
                    // update veto!
                    dMonDisplVetoVeto[1] = adesso.AddMilliseconds(delayShowLogMs);

                    if (!ShouldUpdateUI()) return;
                    // se scaduto veto --> display!
                    lblOutMessage.Text = String.Join(Environment.NewLine, dMonValues[1]);
                }
            }
        }

        /// <summary>
        /// Stringa dati monitoraggio mostrata (2 centro)...
        /// </summary>
        public string dataMonitor_2
        {
            set
            {
                DateTime adesso = DateTime.Now;
                // salvo nell'array...
                dMonValues[2].Insert(0, value);
                // se supero limite --> trim!
                if (dMonValues[2].Count > nLine2show)
                {
                    dMonValues[2] = dMonValues[2].Take(nLine2show).ToList();
                }
                if (dMonDisplVetoVeto[2] < adesso)
                {
                    // update veto!
                    dMonDisplVetoVeto[2] = adesso.AddMilliseconds(delayShowLogMs);

                    if (!ShouldUpdateUI()) return;
                    // se scaduto veto --> display!
                    lblOutMessage2.Text = String.Join(Environment.NewLine, dMonValues[2]);
                }
            }
        }

        /// <summary>
        /// Stringa dati monitoraggio mostrata (3 dx)...
        /// </summary>
        public string dataMonitor_3
        {
            set
            {
                DateTime adesso = DateTime.Now;
                // salvo nell'array...
                dMonValues[3].Insert(0, value);
                // se supero limite --> trim!
                if (dMonValues[3].Count > nLine2show)
                {
                    dMonValues[3] = dMonValues[3].Take(nLine2show).ToList();
                }
                if (dMonDisplVetoVeto[3] < adesso)
                {
                    // update veto!
                    dMonDisplVetoVeto[3] = adesso.AddMilliseconds(delayShowLogMs);

                    if (!ShouldUpdateUI()) return;
                    // se scaduto veto --> display!
                    lblOutMessage3.Text = String.Join(Environment.NewLine, dMonValues[3]);
                }
            }
        }

        /// <summary>
        /// label del numero di record processati (libera)
        /// </summary>
        public string dataProcLabel
        {
            get
            {
                return lblDataProc.Text;
            }
            set
            {
                lblDataProc.Text = value;
            }
        }

        /// <summary>
        /// File configurazione default
        /// </summary>
        public string defConfFilePath
        {
            get
            {
                return Path.Combine(utils.confDir, $"{CurrIOB}.ini");
            }
        }

        /// <summary>
        /// File configurazione json (completo) se presente
        /// </summary>
        public string defConfFilePathJson
        {
            get
            {
                return Path.Combine(utils.confDir, $"{CurrIOB}.json");
            }
        }

        /// <summary>
        /// File configurazione yaml (completo) se presente
        /// </summary>
        public string defConfFilePathYaml
        {
            get
            {
                return Path.Combine(utils.confDir, $"{CurrIOB}.yaml");
            }
        }

        public bool enableEditMes2Plc { get; set; }

        public long evQueueLen
        {
            set
            {
                qEvLen = value;
                lblQueueLen.Text = qEvLen.ToString();
                showQueueData();
                // se supero max precedente, ed è > 10... loggo!
                if (qEvLen > maxEvQueue && qEvLen > 10)
                {
                    maxEvQueue = qEvLen;
                    lgInfo($"[WARN] Coda EV di {value} record");
                }
                else
                {
                    maxEvQueue--;
                    maxEvQueue = maxEvQueue < qEvLen ? qEvLen : maxEvQueue;
                }
            }
            get
            {
                return qEvLen;
            }
        }

        public long flQueueLen
        {
            set
            {
                qFlLen = value;
                lblQueueFLogLen.Text = qFlLen.ToString();
                showQueueData();
                // se supero max precedente, ed è > 10... loggo!
                if (qFlLen > maxFlQueue && qFlLen > 10)
                {
                    maxFlQueue = qFlLen;
                    lgInfo($"[WARN] Coda FLog di {value} record");
                }
                else
                {
                    maxFlQueue--;
                    maxFlQueue = maxFlQueue < qFlLen ? qFlLen : maxFlQueue;
                }
            }
            get
            {
                return qFlLen;
            }
        }

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

        public string lblCncText
        {
            get => lblCNC.Text;
            set => lblCNC.Text = value;
        }

        public string lblSrvUrlText
        {
            get => lblSrvUrl.Text;
            set => lblSrvUrl.Text = value;
        }

        /// <summary>
        /// Logwatcher (in modalità "accodamento in testa" ultimi messaggi...)
        /// </summary>
        public string logWatcher
        {
            get
            {
                return lblLogfile.Text;
            }
            set
            {
                try
                {
                    // aggiungo in testa la NUOVA stringa (eventualmente multiline)
                    logWatchString.Enqueue(value);
                    // se supero limite --> trim!
                    string sTemp = "";
                    while (logWatchString.Count > nLine2show)
                    {
                        logWatchString.TryDequeue(out sTemp);
                        //logWatchString = logWatchString.Take(nLine2show).ToList();
                    }
                    DateTime adesso = DateTime.Now;
                    if (logWatchWriteVeto < adesso)
                    {
                        if (!ShouldUpdateUI()) return;
                        this.UIThread(delegate
                        {
                            lblLogfile.Text = string.Join(Environment.NewLine, logWatchString);
                            lblLogfile.Refresh();
                        });

                        logWatchWriteVeto = adesso.AddMilliseconds(delayShowLogMs);
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Errore in esecuzione logWatcher{Environment.NewLine}--> {value}");
                    if (isVerboseLog)
                    {
                        lgError($"{exc}");
                    }
                }
            }
        }

        public long msQueueLen
        {
            set
            {
                qMsLen = value;
                lblQueueMessLen.Text = qMsLen.ToString();
                showQueueData();
                // se supero max precedente, ed è > 10... loggo!
                if (qMsLen > maxMsQueue && qMsLen > 10)
                {
                    maxMsQueue = qMsLen;
                    lgInfo($"[WARN] Coda Ms di {value} record");
                }
                else
                {
                    maxMsQueue--;
                    maxMsQueue = maxMsQueue < qMsLen ? qMsLen : maxMsQueue;
                }
            }
            get
            {
                return qMsLen;
            }
        }

        /// <summary>
        /// Numero max linee da mostrare (da controllo)...
        /// </summary>
        public int nLine2show
        {
            get
            {
                int answ = 5;
                try
                {
                    Int32.TryParse(nLines.Text, out answ);
                }
                catch
                { }
                return answ;
            }
            set
            {
                nLines.Text = value.ToString();
            }
        }

        public long rtrQueueLen
        {
            set
            {
                qRTrLen = value;
                lblQueueRwTrLog.Text = qRTrLen.ToString();
                showQueueData();
                // se supero max precedente, ed è > 10... loggo!
                if (qRTrLen > maxRwTrQueue && qRTrLen > 10)
                {
                    maxRwTrQueue = qRTrLen;
                    lgInfo($"[WARN] Coda RawTransf di {value} record");
                }
                else
                {
                    maxRwTrQueue--;
                    maxRwTrQueue = maxRwTrQueue < qRTrLen ? qRTrLen : maxRwTrQueue;
                }
            }
            get
            {
                return qRTrLen;
            }
        }

        /// <summary>
        /// Imposta COLORE SFONDO x Semaforo IN
        /// </summary>
        public Semaforo sIN
        {
            get
            {
                return _sIN;
            }
            set
            {
                _sIN = value;
                var newColor = decSemaforo(value);
                if (newColor != bIN.BackColor)
                {
                    if (!ShouldUpdateUI()) return;
                    this.UIThread(delegate
                    {
                        bIN.BackColor = newColor;
                        bIN.Refresh();
                    });
                }
            }
        }

        /// <summary>
        /// Imposta COLORE SFONDO x Semaforo OUT
        /// </summary>
        public Semaforo sOUT
        {
            get
            {
                return _sOUT;
            }
            set
            {
                _sOUT = value;
                var newColor = decSemaforo(value);
                if (newColor != bOUT.BackColor)
                {
                    if (!ShouldUpdateUI()) return;
                    this.UIThread(delegate
                    {
                        bOUT.BackColor = newColor;
                        bOUT.Refresh();
                    });
                }
            }
        }

        /// <summary>
        /// Task watcher (in modalità "accodamento in testa" ultimi messaggi...)
        /// </summary>
        public string taskWatcher
        {
            get
            {
                return lblTaskLog.Text;
            }
            set
            {
                try
                {
                    logTaskString.Insert(0, value);
                    // se supero limite --> trim!
                    if (logTaskString.Count > nLine2show)
                    {
                        logTaskString = logTaskString.Take(nLine2show).ToList();
                    }
                    if (!ShouldUpdateUI()) return;
                    this.UIThread(delegate
                    {
                        lblTaskLog.Text = string.Join(Environment.NewLine, logTaskString);
                        lblTaskLog.Refresh();
                    });
                }
                catch (Exception exc)
                {
                    lgError($"Errore in esecuzione taskWatcher{Environment.NewLine}--> {value}");
                    if (isVerboseLog)
                    {
                        lgError($"{exc}");
                    }
                }
            }
        }

        /// <summary>
        /// tipo di adapter prescelto...
        /// </summary>
        public tipoAdapter tipoScelto
        {
            get => IOBConfFull != null ? IOBConfFull.General.IobType : tipoAdapter.ND;
        }

        public long ulQueueLen
        {
            set
            {
                qUlLen = value;
                lblQueueULog.Text = qUlLen.ToString();
                showQueueData();
                // se supero max precedente, ed è > 10... loggo!
                if (qUlLen > maxUlQueue && qUlLen > 10)
                {
                    maxUlQueue = qUlLen;
                    lgInfo($"[WARN] Coda UL di {value} record");
                }
                else
                {
                    maxUlQueue--;
                    maxUlQueue = maxUlQueue < qUlLen ? qUlLen : maxUlQueue;
                }
            }
            get
            {
                return qUlLen;
            }
        }

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// Decodifica colore da valore semaforico
        /// </summary>
        /// <param name="valore"></param>
        /// <returns></returns>
        public static Color decSemaforo(Semaforo valore)
        {
            Color colore = Color.LightGray;
            switch (valore)
            {
                case Semaforo.SV:
                    colore = Color.LightGreen;
                    break;

                case Semaforo.SG:
                    colore = Color.LightGoldenrodYellow;
                    break;

                case Semaforo.SR:
                    colore = Color.Red;
                    break;

                case Semaforo.SS:
                    colore = Color.DarkGray;
                    break;

                default:
                    colore = Color.LightGray;
                    break;
            }
            return colore;
        }

        /// <summary>
        /// Avvio l'adapter
        /// </summary>
        /// <param name="resetQueue">indica se sia richiesto di SVUOTARE le code delle info</param>
        public void AvviaAdapter(bool resetQueue)
        {
            displayTaskAndLog("Adapter starting", true);
            // se NON sta girando...
            if (!iobObj.adpRunning)
            {
                iobObj.startAdapter(resetQueue);
                displayTaskAndLog("Adapter started!");

                // fix buttons start/stop/dump
                start.Enabled = false;
                stop.Enabled = true;
                restart.Enabled = true;
                // 2026.01.02 fix timers al restart
                gather.Enabled = true;

                // gestione worker
                StartWorker();
                displayTaskAndLog("Start Timers", true);

                // inizializzo le scadenze dei timers...
                DateTime adesso = DateTime.Now;

                foreach (gatherCycle item in Enum.GetValues(typeof(gatherCycle)))
                {
                    // setup iniziale deterministico dei 2 obj gestione
                    TimerMachine.Veto.Add(item, adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0)));
                    TimerMachine.CycleCount.Add(item, 0);
                    TimerMachine.CycleElaps.Add(item, 0);

                    TimerServer.Veto.Add(item, adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0)));
                    TimerServer.CycleCount.Add(item, 0);
                    TimerServer.CycleElaps.Add(item, 0);
                }
                sendStartFLog = utils.CRB("sendStartFLog");
                displayTaskAndLog("Adapter Running...", true);
                // init max queue
                maxEvQueue = 1;
                maxFlQueue = 1;

                try
                {
                    if (sendStartFLog)
                    {
                        // segnalo reboot (programma)...
                        iobObj.QueueFLog.Enqueue(iobObj.qEncodeFLog("IOB-STATUS", "IOB Adapter Started"));
                    }
                }
                catch (Exception exc)
                {
                    lgError(string.Format("EXCEPTION in fase di chiamata URL di segnalazione AVVIO IOB:{0}{1}", Environment.NewLine, exc));
                }
            }
            else
            {
                displayTaskAndLog("Adapter STILL Running...", true);
            }
        }

        /// <summary>
        /// Registra variazione error delay in caso di errori o successi
        /// </summary>
        /// <param name="topic"></param>
        /// <param name="currCount"></param>
        /// <param name="delta"></param>
        public void ChangeErrorDelay(string topic, int currCount, int delta)
        {
            _errorDelay += delta;
            // se fosse negativo --> riporto a zero!
            _errorDelay = _errorDelay < 0 ? 0 : _errorDelay;
            // registro LOG variazione
            if (delta > 0)
            {
                lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId}] | +++ delay {_errorDelay} | {topic} | err: {currCount} | {DateTime.Now:HH:mm:ss.fff}");
            }
            else
            {
                if (_errorDelay > 0)
                {
                    lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId}] | --- delay {_errorDelay} | {topic} | err: {currCount} | {DateTime.Now:HH:mm:ss.fff}");
                }
            }
        }

        /// <summary>
        /// mostra un testo sulla status bar + LOG
        /// </summary>
        /// <param name="txt2show"></param>
        /// <param name="forceDebug"></param>
        public void displayTaskAndLog(string txt2show, bool forceDebug = false)
        {
            if (forceDebug)
            {
                lgDebug(txt2show);
            }
            else
            {
                lgInfo(txt2show);
            }
            if (!ShouldUpdateUI()) return;
            lblStatus.Text = txt2show;
            lblStatus.Invalidate();
        }

        /// <summary>
        /// Ferma l'adapter
        /// </summary>
        /// <param name="tryRestart">
        /// determina se si debba tentare riavvio automatico (per caduta connessione)
        /// </param>
        /// <param name="forceDequeue">indica se sia richiesto di SVUOTARE le code delle info</param>
        /// <param name="updateForm">indica se aggiornare la form prima di fermare</param>
        public void fermaAdapter(bool tryRestart, bool forceDequeue, bool updateForm)
        {
            lgInfo($"fermaAdapter | tryRestart: {tryRestart} | forceDequeue: {forceDequeue} | updateForm: {updateForm}");
            //fermaTutto(false, tryRestart, forceDequeue, updateForm);
            fermaTutto(false, tryRestart, forceDequeue, updateForm);
        }

        /// <summary>
        /// Mostra update delle statistiche di comunicazione (numero chiamate, tempo medio...)
        /// </summary>
        /// <param name="txt2show"></param>
        public void updateComStats(string txt2show)
        {
            if (!ShouldUpdateUI()) return;
            lblComStats.Text = string.Format("{0} | ", txt2show);
        }

        /// <summary>
        /// Effettua update form una volta ricevuto OBJ che contiene le varie innovazioni...
        /// </summary>
        /// <param name="currDispData"></param>
        public void updateFormDisplay(newDisplayData currDispData)
        {
            if (!ShouldUpdateUI()) return;
            this.UIThread(delegate
            {
                // ciclo x ogni oggetto x caricare le innovazioni...
                if (currDispData != null && currDispData.hasData)
                {
                    DateTime adesso = DateTime.Now;
                    // RealTime display
                    if (!string.IsNullOrWhiteSpace(currDispData.newInData))
                    {
                        dataMonitor_0 = currDispData.newInData;
                    }
                    if (!string.IsNullOrWhiteSpace(currDispData.newSignalData))
                    {
                        dataMonitor_1 = currDispData.newSignalData;
                    }
                    if (!string.IsNullOrWhiteSpace(currDispData.newFLogData))
                    {
                        dataMonitor_3 = currDispData.newFLogData;
                    }
                    if (!string.IsNullOrWhiteSpace(currDispData.newUrlCallData))
                    {
                        dataMonitor_2 = currDispData.newUrlCallData;
                    }

                    // Bitmap lettura attuale
                    if (currDispData.counter >= 0)
                    {
                        if (logCounterVeto < adesso || lblCounter.Text != $"{currDispData.counter}")
                        {
                            lblCounter.Text = $"{currDispData.counter}";
                            lblCounter.Refresh();
                            logCounterVeto = adesso.AddMilliseconds(delayShowLogMs);
                        }
                    }
                    // Bitmap lettura attuale
                    if (!string.IsNullOrWhiteSpace(currDispData.currBitmap))
                    {
                        lblBitmap.Text = currDispData.currBitmap;
                        lblBitmap.Refresh();
                    }
                    // LiveLog
                    if (!string.IsNullOrWhiteSpace(currDispData.newLiveLogData))
                    {
                        logWatcher = currDispData.newLiveLogData;
                    }
                    // semafori
                    if (currDispData.semOut != Semaforo.ND)
                    {
                        // aggiorno SE diverso
                        if (sOUT != currDispData.semOut)
                        {
                            sOUT = currDispData.semOut;
                        }
                    }
                    if (currDispData.semIn != Semaforo.ND)
                    {
                        //aggiorno SE diverso
                        if (sIN != currDispData.semIn)
                        {
                            sIN = currDispData.semIn;
                        }
                    }
                }
            });
        }

        public void WriteTextSafe(string text)
        {
            if (!ShouldUpdateUI()) return;
            this.UIThread(delegate
            {
                // accoda messaggio in gestione
                dataMonitor_3 = text;
            });
        }

        #endregion Public Methods

        #region Protected Fields

        protected static string baseUrl = @"https://liman.egalware.com/MP/IO/";

        /// <summary>
        /// Data-Ora prima apertura FORM...
        /// </summary>
        protected DateTime firstStart;

        /// <summary>
        /// Oggetto ultimo inviato stato IOB x REDIS
        /// </summary>
        protected IobWinStatus lastIobStatus = new IobWinStatus() { lastUpdate = DateTime.Now.AddHours(-1), lastDataOut = DateTime.Now.AddHours(-1) };

        /// <summary>
        /// Oggetto ultimo inviato stato MP-IO x REDIS
        /// </summary>
        protected ServerMpStatus lastSrvStatus = new ServerMpStatus();

        /// <summary>
        /// ultimo tentativo riavvio...
        /// </summary>
        protected DateTime lastStartTry;

        /// <summary>
        /// Generatore numeri random
        /// </summary>
        protected Random rndGen = new Random();

        /// <summary>
        /// Contatore campionamento memoria
        /// </summary>
        protected int sampleMemCount;

        /// <summary>
        /// Indica se inviare avvio adapter con FluxLog
        /// </summary>
        protected bool sendStartFLog = false;

        /// <summary>
        /// Temnpo attesa std in MS
        /// </summary>
        protected int waitRecMSec = 30000;

        #endregion Protected Fields

        #region Protected Properties

        /// <summary>
        /// Valore protected comunicazione PLC
        /// </summary>
        protected bool _commPlcActive { get; set; } = false;

        /// <summary>
        /// Valore protetto stato comunicazione
        /// </summary>
        protected int _commSrvActive { get; set; } = 0;

        /// <summary>
        /// Valore protected semaforo IN
        /// </summary>
        protected Semaforo _sIN { get; set; } = Semaforo.ND;

        /// <summary>
        /// Valore protected semaforo OUT
        /// </summary>
        protected Semaforo _sOUT { get; set; } = Semaforo.ND;

        /// <summary>
        /// Codice IOB della macchina cui connettersi (x scegliere corretto file di conf...)
        /// </summary>
        protected string CurrIOB { get; set; }

        protected int delayShowLogMs { get; set; } = utils.CRI("delayShowLogMs");

        /// <summary>
        /// Dictionary dei divieti del datamonitor
        /// </summary>
        protected Dictionary<int, DateTime> dMonDisplVetoVeto { get; set; } = new Dictionary<int, DateTime>();

        /// <summary>
        /// array degli oggetti datamonitor da mostrare
        /// </summary>
        protected Dictionary<int, List<string>> dMonValues { get; set; } = new Dictionary<int, List<string>>();

        /// <summary>
        /// Veto a NUOVE scritture in COUNTER...
        /// </summary>
        protected DateTime logCounterVeto { get; set; } = DateTime.Now;

        /// <summary>
        /// Lista String da mostrare quale TaskLog corrente...
        /// </summary>
        protected List<string> logTaskString { get; set; } = new List<string>();

        /// <summary>
        /// Lista String da mostrare quale WatchLog corrente...
        /// </summary>
        protected ConcurrentQueue<string> logWatchString { get; set; } = new ConcurrentQueue<string>();

        /// <summary>
        /// Veto a NUOVE scritture in logWatch...
        /// </summary>
        protected DateTime logWatchWriteVeto { get; set; } = DateTime.Now;

        protected long maxAlQueue { get; set; }

        protected long maxEvQueue { get; set; }

        protected long maxFlQueue { get; set; }

        protected long maxMsQueue { get; set; }

        protected long maxRwTrQueue { get; set; }

        protected long maxUlQueue { get; set; }

        protected long qAlLen { get; set; }

        protected long qEvLen { get; set; }

        protected long qFlLen { get; set; }

        protected long qMsLen { get; set; }

        protected long qRTrLen { get; set; }

        protected long qUlLen { get; set; }

        protected long totQueue
        {
            get
            {
                return qEvLen + qFlLen + qAlLen + qMsLen + qUlLen;
            }
        }

        /// <summary>
        /// URL per salvare i file dell'IOB (SENZA IOB)
        /// </summary>
        protected string urlUploadFile
        {
            get => $"http://{IOBConfFull.MapoMes.IpAddr}{IOBConfFull.MapoMes.BaseAppUrl}IOB/uploadFile/";
        }

        /// <summary>
        /// URL per salvare i file dell'IOB su CLOUD (SENZA IOB)
        /// </summary>
        protected string urlUploadFileCloud
        {
            get => $"{baseUrl}IOB/uploadFile/";
        }

        #endregion Protected Properties

        #region Protected Methods

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// Esegue logica di init async x IOB
        /// </summary>
        /// <returns></returns>
        protected async Task<bool> iobInitAsync()
        {
            try
            {
                this.Cursor = Cursors.WaitCursor;
                await iobObj.InitializeAsync();
                this.Cursor = Cursors.Default;
            }
            catch (Exception ex)
            {
                lgError($"Inizializzazione fallita per {tipoScelto}: {ex.Message}");
                iobObj = null; // Evitiamo di usare un oggetto malfunzionante
                return false;
            }

            return true;
        }

        /// <summary>
        /// Gestione evento refresh
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void IobObj_eh_refreshed(object sender, iobRefreshedEventArgs e)
        {
            if (!ShouldUpdateUI()) return;
            // aggiorno!
            this.UIThread(delegate
            {
                updateFormDisplay(e.DisplayDataObject);
            });
        }

        /// <summary>
        /// Effettua logging DEBUG corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="txt2log"></param>
        protected void lgDebug(string txt2log)
        {
            lg.Factory.Configuration.Variables["codIOB"] = this.CurrIOB;//IOBConfFull.General.FilenameIOB ??this.CurrIOB;
            lg.Debug(txt2log);
            // salvo anche in logwatcher...
            newDisplayData currDispData = new newDisplayData();
            currDispData.newLiveLogData = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} | INFO | {txt2log}";
            updateFormDisplay(currDispData);
        }

        /// <summary>
        /// Effettua logging ERROR corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="txt2log"></param>
        protected void lgError(string txt2log)
        {
            if (!string.IsNullOrEmpty(txt2log))
            {
                lg.Factory.Configuration.Variables["codIOB"] = this.CurrIOB;//IOBConfFull.General.FilenameIOB ??this.CurrIOB;
                lg.Error(txt2log);
                // salvo anche in logwatcher... SE non si dimostra ricorsivo...
                if (!txt2log.Contains("logWatcher"))
                {
                    newDisplayData currDispData = new newDisplayData();
                    currDispData.newLiveLogData = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} | ERROR | {txt2log}";
                    updateFormDisplay(currDispData);
                }
            }
        }

        /// <summary>
        /// Effettua logging INFO corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="txt2log"></param>
        protected void lgInfo(string txt2log)
        {
            lg.Factory.Configuration.Variables["codIOB"] = this.CurrIOB;//IOBConfFull.General.FilenameIOB ??this.CurrIOB;
            lg.Info(txt2log);
            // salvo anche in logwatcher...
            newDisplayData currDispData = new newDisplayData();
            currDispData.newLiveLogData = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} | INFO | {txt2log}";
            updateFormDisplay(currDispData);
        }

        /// <summary>
        /// Effettua logging TRACE corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="txt2log"></param>
        protected void lgTrace(string txt2log)
        {
            lg.Factory.Configuration.Variables["codIOB"] = this.CurrIOB;//IOBConfFull.General.FilenameIOB ??this.CurrIOB;
            lg.Trace(txt2log);
            // salvo anche in logwatcher...
            newDisplayData currDispData = new newDisplayData();
            currDispData.newLiveLogData = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} | TRACE | {txt2log}";
            updateFormDisplay(currDispData);
        }

        /// <summary>
        /// carica IOB richiesto
        /// </summary>
        protected virtual async Task loadIobType()
        {
            if (IOBConfFull != null)
            {
                switch (tipoScelto)
                {
                    case tipoAdapter.ND:
                    default:
                        iobObj = new Iob.Simula(this, IOBConfFull);
                        start.Enabled = false;
                        break;
                }
                if (!await iobInitAsync())
                {
                    return;
                }
                UpdateDisplTypeIobSel();
            }
        }

        /// <summary>
        /// Init form (grafica) con helper x testi e varie
        /// </summary>
        protected void myGraphInitForm()
        {
            lblStatus.Text = "Loading";

            // aggiunta tooltip x send forzato
            ToolTip ttForceSend = new ToolTip();

            // imposto delay.
            ttForceSend.AutoPopDelay = 3000;
            ttForceSend.InitialDelay = 500;
            ttForceSend.ReshowDelay = 500;
            // sempre visibile (che sia o meno attiva la form).
            ttForceSend.ShowAlways = true;

            // imposto tooltip
            ttForceSend.SetToolTip(this.chkForceDequeue, "Forza invio eventi allo stop");
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            CloseTimers();
            base.OnFormClosing(e);
        }

        /// <summary>
        /// impostazione valori defaults
        /// </summary>
        protected void setDefaults()
        {
            stop.Enabled = false;

            alQueueLen = 0;
            flQueueLen = 0;
            msQueueLen = 0;
            rtrQueueLen = 0;
            ulQueueLen = 0;
            nLine2show = utils.CRI("numRowConsole");
        }

        /// <summary>
        /// MOstra info su coda complessiva
        /// </summary>
        protected void showQueueData()
        {
            lblQueueLenTop.Text = totQueue == 0 ? "realtime" : $"ev: {qEvLen} | flog: {qFlLen} | tot: {totQueue}";
        }

        /// <summary>
        /// Update display info tipo IOB selezionato
        /// </summary>
        protected void UpdateDisplTypeIobSel()
        {
            lblCNC.Text = $"CNC: {IOBConfFull.General.IobType} [{IOBConfFull.Device.Connect.IpAddr}:{IOBConfFull.Device.Connect.Port}]";
            lblSrvUrlText = $"SRV: {IOBConfFull.MapoMes.IpAddr} | BaseURL: {IOBConfFull.MapoMes.ApiUrl("")}";

            // aggancio evento refresh
            iobObj.eh_refreshed -= IobObj_eh_refreshed;
            iobObj.eh_refreshed += IobObj_eh_refreshed;

            // carico i default values su interfaccia
            setDefaults();

            displayTaskAndLog($"Caricata conf per adapter {tipoScelto}");
        }

        #endregion Protected Methods

        #region Private Fields

        /// <summary>
        /// Task factory single threaded
        /// </summary>
        private static readonly TaskFactory _singleFactory = new TaskFactory(_singleScheduler);

        /// <summary>
        /// Scheduler single threaded
        /// </summary>
        private static readonly SingleThreadTaskScheduler _singleScheduler = new SingleThreadTaskScheduler("IOB-SINGLE");

        private readonly object _lock = new object();

        /// <summary>
        /// Usato come semaforo x evitare doppio uso risorse threaded
        /// </summary>
        private readonly SemaphoreSlim _plcLock = new SemaphoreSlim(1, 1);

        /// <summary>
        /// Cancellation token x processi verso macchina
        /// </summary>
        private CancellationTokenSource _ctsMachine;

        /// <summary>
        /// Cancellation tocken x processi verso server
        /// </summary>
        private CancellationTokenSource _ctsServer;

        /// <summary>
        /// Ritardo per errori (extra nei cicli)
        /// </summary>
        private int _errorDelay = 0;

        // Per thread-safety
        private bool _isSuspended = false;

        private FormWindowState _lastState = FormWindowState.Minimized;

        private Task _workerTask;

        private bool stopForced = false;

#if false
        /// <summary>
        /// Contatore chiamate timers x debug timing
        /// </summary>
        private Dictionary<gatherCycle, int> TimersCycleCount = new Dictionary<gatherCycle, int>();

        /// <summary>
        /// Contatore durata exec x debug timing
        /// </summary>
        private Dictionary<gatherCycle, double> TimersCycleElaps = new Dictionary<gatherCycle, double>();

        /// <summary>
        /// Dizionario scadenza timers interni x esecuzione dei vari task a differente frequenza di chiamata
        /// </summary>
        private Dictionary<gatherCycle, DateTime> TimersVeto = new Dictionary<gatherCycle, DateTime>(); 
#endif

        /// <summary>
        /// Obj gestione Timer per esecuzione Task con Machine
        /// </summary>
        private TimerMan TimerMachine = new TimerMan(TimerMan.ContextType.Machine);

        /// <summary>
        /// Obj gestione Timer per esecuzione Task con Server
        /// </summary>
        private TimerMan TimerServer = new TimerMan(TimerMan.ContextType.Server);

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

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

        #endregion Private Fields

        #region Private Delegates

        private delegate void SafeCallDelegate(string text);

        #endregion Private Delegates

        #region Private Methods

        /// <summary>
        /// Esecuzione dei metodi in modalità async dopo il load della Form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void AdapterForm_Load(object sender, EventArgs e)
        {
            // se abilitato autoload conf leggo file corretto...
            if (utils.CRB("autoLoadConf"))
            {
                try
                {
                    if (File.Exists(defConfFilePathYaml))
                    {
                        loadYamlFile(defConfFilePathYaml);
                        await sendConfFile(defConfFilePathYaml);
                    }
                    else
                    {
                        loadIniFile(defConfFilePath);
                        await sendConfFile(defConfFilePath);
                    }
                    lgInfo("INI LOADED");
                }
                catch (Exception exc)
                {
                    displayTaskAndLog(string.Format("Eccezione in autoLoadConf: {0}", exc));
                }
            }

            if (IOBConfFull == null || !utils.CRB("autoLoadConf"))
            {
                IOBConfFull = new IobConfTree();
                await loadIobType();
                displayTaskAndLog("Waiting for config file selection");
            }

            // ricarica stats da redis
            string callKey = iobObj.redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CallStats");
            await CallMetricsCollector.LoadFromRedisAsync(iobObj.redisMan.currDb, callKey);


            // Start timer periodico comunicazione
            gather.Interval = IOBConfFull.General.Timers.MsUI;
            gather.Enabled = true;
            displayTaskAndLog($"Main UI timer set: {gather.Interval}ms", true);

            // Start timer periodico interfaccia
            displTimer.Interval = utils.CRI("timerIntMs");
            displTimer.Enabled = true;
            displayTaskAndLog("UI Program Running", true);

            // check oggetto not null
            if (iobObj != null)
            {
                try
                {
                    // verifico server online...
                    bool srvAlive = iobObj.CheckServerAlive();
                    if (srvAlive)
                    {
                        string sendKey = "SendReboot";
                        bool sendEnab = iobObj.CheckSendVeto(sendKey, TimeSpan.FromMinutes(60));
                        // se abilitato faccio invio e salvo nuovo valore
                        if (sendEnab)
                        {
                            // salvo nuovo valore invio
                            iobObj.LastSendSet(sendKey, DateTime.Now);
                            // segnalo reboot (programma - url file)...
                            await Iob.Generic.callUrl(iobObj.urlReboot, true);
                        }
                    }
                    else
                    {
                        displayTaskAndLog("AdapterForm: Server OFFLINE");
                    }
                }
                catch (Exception exc)
                {
                    lgError($"AdapterForm: EXCEPTION in fase di chiamata URL di reboot:{iobObj.urlReboot}{Environment.NewLine}{exc}");
                }
                displayTaskAndLog($"Main workerTimer set: {IOBConfFull.General.Timers.MsVHF}ms", true);
            }
            displayTaskAndLog("Adapter Form OK", true);
        }

        private void btnForceAutoOdl_Click(object sender, EventArgs e)
        {
            iobObj.forceResetOdl();
        }

        private void BtnOpenLog_Click(object sender, EventArgs e)
        {
            try
            {
                string logPath = $"{System.AppDomain.CurrentDomain.BaseDirectory}logs\\{CurrIOB}\\{DateTime.Today:yyyy-MM-dd}.log";
                Process.Start(logPath);
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in show log (MAIN):{Environment.NewLine}{exc}");
            }
        }

        private void BtnSendPLC_Click(object sender, EventArgs e)
        {
            // invia a MES il parametro selezionato (es ART / ODL / PRG NAME, come task2exe..)
            Dictionary<string, string> forcedTask = new Dictionary<string, string>();
            // guardo i parametri...
            if (!string.IsNullOrEmpty(txtValue.Text))
            {
                forcedTask.Add(cmbParamValues.SelectedValue.ToString(), txtValue.Text);
            }
            // chiedo esecuzione task! forzato SENZA tavola
            _ = iobObj.ProcessTask(forcedTask, "");
            chkEdit.Checked = false;
            toggleEditMes2Plc();
        }

        private void checkAssignSize()
        {
            int valSize = 0;
            int.TryParse(txtMReadSize.Text, out valSize);
            // verifico sia valida con lenght array...
            int maxSize = iobObj.RawInput.Length - iobObj.RawDataInputStart;
            valSize = valSize > maxSize ? maxSize : valSize;
            // riporto
            iobObj.RawDataInputSize = valSize;
        }

        private void checkAssignStart()
        {
            int valStart = 0;
            int.TryParse(txtMReadStart.Text, out valStart);
            // verifico sia valida con start/lenght array...
            int maxStart = iobObj.RawInput.Length - 1;
            valStart = valStart > maxStart ? maxStart : valStart;
            // riporto
            iobObj.RawDataInputStart = valStart;
        }

        private void checkEditMes2Plc()
        {
            cmbParamValues.Enabled = enableEditMes2Plc;
            txtValue.Enabled = enableEditMes2Plc;
            if (enableEditMes2Plc)
            {
                // aggiorno (se possibile) i parametri selezionabili...
                fixComboParameters();
            }
        }

        private void checkSampleMem()
        {
            // decremento contatore...
            sampleMemCount--;
            if (sampleMemCount <= 0)
            {
                sampleMemCount = utils.CRI("sampleMemCount");
                // avvio fase raccolta dati e invio con adapter
                iobObj.saveMemDump(dumpType.SAMPLE);
            }
        }

        /// <summary>
        /// Verifica scadenza task nell'oggetto Pool
        /// </summary>
        /// <returns></returns>
        private async Task checkScadPoolAsync()
        {
            DateTime adesso = DateTime.Now;
            bool sendDone = false;
            // ciclo su tutti i timers enum superiori a VHF
            var priorities = (gatherCycle[])Enum.GetValues(typeof(gatherCycle));
            // prendo solo > VHF e ordino discendenti x dare priorità ai più rari...
            var ordered = priorities.Where(x => x > gatherCycle.VHF).OrderByDescending(x => x);
            foreach (gatherCycle item in ordered)
            {
                if (TimerServer.Veto.ContainsKey(item))
                {
                    if (TimerServer.Veto[item] <= adesso)
                    {
                        // se non ho già processato a questo giro...
                        if (!sendDone)
                        {
                            TimerServer.CycleCount[item]++;

                            Stopwatch sw = Stopwatch.StartNew();
                            await iobObj.doServerTaskAsync(item);
                            sw.Stop();

                            sendDone = true;
                            // metto una piccola attesa se ho altre scadenze
                            TimerServer.CycleElaps[item] += sw.Elapsed.TotalMilliseconds;
                            // metto nuova scadenza perturbata 10%
                            TimerServer.Veto[item] = adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0.1));
                        }
                        // log + reset se ho contato ALMENO 100 exec o il tempo totale supera 1000msec
                        int limMs = 10000;
                        double totalMs = TimerServer.CycleElaps.ContainsKey(item) ? TimerServer.CycleElaps[item] : 0;
                        if (TimerServer.CycleCount[item] >= 100 || totalMs > limMs)
                        {
                            int nCount = Math.Max(TimerServer.CycleCount[item], 1);
                            lgDebug($"Timer.{TimerServer.Contesto} | checkScadPoolAsync | {item} | {totalMs / nCount:F1}ms x {nCount}");
                            TimerServer.CycleCount[item] = 0;
                            TimerServer.CycleElaps[item] = 0;
                        }
                    }
                }
                else
                {
                    TimerServer.Veto.Add(item, adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0)));
                    TimerServer.CycleCount.Add(item, 0);
                    if (!TimerServer.CycleElaps.ContainsKey(item))
                    {
                        TimerServer.CycleElaps.Add(item, 0);
                    }
                }
            }
        }


        /// <summary>
        /// Verifica scadenza task Machine
        /// </summary>
        /// <returns></returns>
        private void checkScadMachine()
        {
            DateTime adesso = DateTime.Now;
            bool sendDone = false;
            // ciclo su tutti i timers enum superiori a VHF
            var priorities = (gatherCycle[])Enum.GetValues(typeof(gatherCycle));
            // prendo solo > VHF e ordino discendenti x dare priorità ai più rari...
            var ordered = priorities.Where(x => x > gatherCycle.VHF).OrderByDescending(x => x);
            foreach (gatherCycle item in ordered)
            {
                if (TimerMachine.Veto.ContainsKey(item))
                {
                    if (TimerMachine.Veto[item] <= adesso)
                    {
                        // se non ho già processato a questo giro...
                        if (!sendDone)
                        {
                            TimerMachine.CycleCount[item]++;
                            Stopwatch sw = Stopwatch.StartNew();

                            iobObj.doMachineTask(item);

                            sw.Stop();
                            sendDone = true;
                            // metto una piccola attesa se ho altre scadenze
                            TimerMachine.CycleElaps[item] += sw.Elapsed.TotalMilliseconds;
                            // metto nuova scadenza perturbata 10%
                            TimerMachine.Veto[item] = adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0.1));
                        }
                        // log + reset se ho contato ALMENO 100 exec o il tempo totale supera 1000msec
                        int limMs = 10000;
                        double totalMs = TimerMachine.CycleElaps.ContainsKey(item) ? TimerMachine.CycleElaps[item] : 0;
                        if (TimerMachine.CycleCount[item] >= 100 || totalMs > limMs)
                        {
                            int nCount = Math.Max(TimerMachine.CycleCount[item], 1);
                            lgDebug($"Timer.{TimerMachine.Contesto} | checkScadMachine | {item} | {totalMs / nCount:F1}ms x {nCount}");
                            TimerMachine.CycleCount[item] = 0;
                            TimerMachine.CycleElaps[item] = 0;
                        }
                    }
                }
                else
                {
                    TimerMachine.Veto.Add(item, adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0)));
                    TimerMachine.CycleCount.Add(item, 0);
                    if (!TimerMachine.CycleElaps.ContainsKey(item))
                    {
                        TimerMachine.CycleElaps.Add(item, 0);
                    }
                }
            }
        }

#if false
        /// <summary>
        /// Verifica scadenza task
        /// </summary>
        /// <param name="enabPool">Indica se siano abilitate operazioni Pool (Thread Safe)</param>
        /// <param name="enabSTID">Indica se siano abilitate le operazioni Single Thread</param>
        /// <returns></returns>
        private async Task checkScad(bool enabPool, bool enabSTID)
        {
            DateTime adesso = DateTime.Now;
            bool sendDone = false;
            // ciclo su tutti i timers enum superiori a VHF
            var priorities = (gatherCycle[])Enum.GetValues(typeof(gatherCycle));
            // prendo solo > VHF e ordino discendenti x dare priorità ai più rari...
            var ordered = priorities.Where(x => x > gatherCycle.VHF).OrderByDescending(x => x);
            foreach (gatherCycle item in ordered)
            {
                if (TimersVeto.ContainsKey(item))
                {
                    if (TimersVeto[item] <= adesso)
                    {
                        // se non ho già processato a questo giro...
                        if (!sendDone)
                        {
                            TimersCycleCount[item]++;
                            Stopwatch sw = Stopwatch.StartNew();

                            // chiamata di processing x frequenza
                            await iobObj.getAndSendAsync(item, enabPool, enabSTID);

                            sw.Stop();
                            sendDone = true;
                            // metto una piccola attesa se ho altre scadenze
                            TimersCycleElaps[item] += sw.Elapsed.TotalMilliseconds;
                            // metto nuova scadenza perturbata 10%
                            TimersVeto[item] = adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0.1));
                        }
                        // log + reset se ho contato ALMENO 100 exec o il tempo totale supera 1000msec
                        int limMs = 10000;
                        double totalMs = TimersCycleElaps.ContainsKey(item) ? TimersCycleElaps[item] : 0;
                        if (TimersCycleCount[item] >= 100 || totalMs > limMs)
                        {
                            int nCount = Math.Max(TimersCycleCount[item], 1);
                            lgDebug($"checkScad | {item} | {totalMs / nCount:F1}ms x {nCount}");
                            TimersCycleCount[item] = 0;
                            TimersCycleElaps[item] = 0;
                        }
                    }
                }
                else
                {
                    TimersVeto.Add(item, adesso.AddMilliseconds(iobObj.IOBConfFull.TimerMs(item, 0)));
                    TimersCycleCount.Add(item, 0);
                    if (!TimersCycleElaps.ContainsKey(item)) TimersCycleElaps.Add(item, 0);
                }
            }
        } 
#endif

        private void ChkEdit_CheckedChanged(object sender, EventArgs e)
        {
            toggleEditMes2Plc();
        }

        /// <summary>
        /// Chiusura adapter
        /// </summary>
        private void closeAdapter()
        {
            lgDebug("closeAdapter | true | false | true | false");
            fermaTutto(true, false, true, false);
        }

        private void CloseTimers()
        {
            displTimer?.Stop();
            displTimer?.Dispose();
            gather?.Stop();
            gather?.Dispose();
            _isSuspended = true;
            _ = StopWorker();
        }

        private void displTimer_Tick(object sender, EventArgs e)
        {
        }


        /// <summary>
        /// Metodo principale esecuzione task in thread background (no interferenza con UI) x processi IO bound
        /// </summary>
        /// <returns></returns>
        private void DoExecMachineTasks()
        {
            // Attende il proprio turno. Se un task è già in corso, questo aspetta.
            _plcLock.Wait();
            try
            {
                // eseguo cicli attivi SOLO se adapter è in EFFETTIVO running...
                if (iobObj.adpRunning)
                {
                    if (iobObj.connectionOk)
                    {
                        DateTime adesso = DateTime.Now;
                        // verifico non ci sia veto comunicazioni lettura...
                        if (iobObj.queueInEnabCurr)
                        {
                            // se richiesto faccio memory DUMP INIZIALE!
                            if (iobObj.doStartMemDump)
                            {
                                lgInfo("Inizio dump memoria");
                                iobObj.saveMemDump(dumpType.STARTUP);
                                // fatto! non ripeto...
                                iobObj.doStartMemDump = false;
                                lgInfo("Finito dump memoria");
                            }
                            // controllo se sia abilitato sampleDump della meoria (periodico)
                            if (iobObj.doSampleMemory)
                            {
                                checkSampleMem();
                            }

                            // MAIN: controllo TUTTE le scadenze Machine...
                            checkScadMachine();

                            // wait opzionale in coda
                            if (utils.CRI("waitEndCycle") > 0)
                            {
                                Thread.Sleep(utils.CRI("waitEndCycle"));
                            }
                        }
                        else
                        {
                            lgTrace($"VETO DoExecMachineTasks.queueInEnabCurr | veto attivo | {adesso:yyyy.MM.dd HH:mm:ss}");
                            iobObj.checkVetoQueueIn();
                        }
                    }
                    else
                    {
                        // qui attende meno...
                        DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec / 2);
                        if (iobObj.adpTryRestart && (DateTime.Now > dtVeto))
                        {
                            lgInfo($"DoExecMachineTasks.Retry Time Elapsed ({waitRecMSec / 2} ms)--> tryConnect");
                            lastStartTry = DateTime.Now;
                            iobObj.tryConnect();
                        }
                    }
                }
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in fase di DoExecMachineTasks: {Environment.NewLine}{exc}");
            }
            finally
            {
                _plcLock.Release();
            }
        }


        /// <summary>
        /// Metodo principale esecuzione task in thread background (no interferenza con UI) x processi IO bound
        /// </summary>
        /// <param name="reqSingle">Richiesta esecuzone su single thread (es FANUC)</param>
        /// <returns></returns>
        private async Task DoExecServerTasksAsync(bool reqSingle)
        {
            // Attende il proprio turno. Se un task è già in corso, questo aspetta.
            await _plcLock.WaitAsync();
            bool doLog = iobObj.periodicLog;
            try
            {
                // check esecuzione SendTask (MsVHF) COMUNQUE...
                await iobObj.doServerTaskAsync(gatherCycle.VHF);
                // eseguo cicli attivi SOLO se adapter è in EFFETTIVO running...
                if (iobObj.adpRunning)
                {
                    if (iobObj.connectionOk)
                    {
                        DateTime adesso = DateTime.Now;
                        // verifico non ci sia veto comunicazioni lettura...
                        if (iobObj.queueInEnabCurr)
                        {

                            // MAIN: controllo TUTTE le scadenze Server...
                            await checkScadPoolAsync();

                            // wait opzionale in coda
                            if (utils.CRI("waitEndCycle") > 0)
                            {
                                await Task.Delay(utils.CRI("waitEndCycle"));
                            }
                        }
                        else
                        {
                            lgTrace($"VETO DoExecServerTasksAsync.queueInEnabCurr | veto attivo | {adesso:yyyy.MM.dd HH:mm:ss}");
                            iobObj.checkVetoQueueIn();
                        }
                    }
#if false
                    else
                    {
                        // qui attende meno...
                        DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec / 2);
                        if (iobObj.adpTryRestart && (DateTime.Now > dtVeto))
                        {
                            if (doLog)
                            {
                                lgInfo($"Retry Time Elapsed ({waitRecMSec / 2} ms)--> tryConnect");
                            }
                            lastStartTry = DateTime.Now;
                            iobObj.tryConnect();
                        }
                    } 
#endif
                }
                else
                {
                    if (doLog)
                    {
                        lgInfo("Adapter stopped");
                    }
                    // verifico SE debba tentare il riavvio, ovvero NON running ma adpTryRestart
                    // e non ho riprovato x oltre waitRecMSec
                    DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec);
                    if (iobObj.adpTryRestart && (DateTime.Now > dtVeto))
                    {
                        lastStartTry = DateTime.Now;
                        AvviaAdapter(chkForceDequeue.Checked);
                    }
                }
            }
            catch (Exception exc)
            {
                lgError($"Eccezione DoExecServerTasksAsync: {Environment.NewLine}{exc}");
            }
            finally
            {
                _plcLock.Release();
            }
        }

#if false
        /// <summary>
        /// Metodo principale esecuzione task in thread background (no interferenza con UI) x processi IO bound
        /// </summary>
        /// <param name="reqSingle">Richiesta esecuzone su single thread (es FANUC)</param>
        /// <returns></returns>
        private async Task DoExecTasksAsync(bool reqSingle)
        {
            // Attende il proprio turno. Se un task è già in corso, questo aspetta.
            await _plcLock.WaitAsync();
            bool doLog = iobObj.periodicLog;
            try
            {
                // imposto variabili x esecuzione separata task Single Thread / Pooled Thread
                bool reqSTE = iobObj.IOBConfFull.General.MachWLoopSingleThread;
                bool enabSTID = reqSTE ? reqSingle : true;
                bool enabPool = !reqSTE;

                if (enabPool)
                {
                    // check esecuzione SendTask (MsVHF) COMUNQUE...
                    await iobObj.getAndSendAsync(gatherCycle.VHF, enabPool, enabSTID);
                }
                // eseguo cicli attivi SOLO se adapter è in EFFETTIVO running...
                if (iobObj.adpRunning)
                {
                    if (iobObj.connectionOk)
                    {
                        DateTime adesso = DateTime.Now;
                        // verifico non ci sia veto comunicazioni lettura...
                        if (iobObj.queueInEnabCurr)
                        {
                            if (enabSTID)
                            {
                                // se richiesto faccio memory DUMP INIZIALE!
                                if (iobObj.doStartMemDump)
                                {
                                    lgInfo("Inizio dump memoria");
                                    iobObj.saveMemDump(dumpType.STARTUP);
                                    // fatto! non ripeto...
                                    iobObj.doStartMemDump = false;
                                    lgInfo("Finito dump memoria");
                                }
                            }
                            if (enabPool)
                            {
                                // controllo se sia abilitato sampleDump della meoria (periodico)
                                if (iobObj.doSampleMemory)
                                {
                                    checkSampleMem();
                                }
                            }

                            // MAIN: controllo TUTTE le scadenze...
                            await checkScad(enabPool, enabSTID);

                            // wait opzionale in coda
                            if (utils.CRI("waitEndCycle") > 0)
                            {
                                await Task.Delay(utils.CRI("waitEndCycle"));
                            }
                        }
                        else
                        {
                            lgTrace($"VETO queueInEnabCurr | veto attivo | {adesso:yyyy.MM.dd HH:mm:ss}");
                            iobObj.checkVetoQueueIn();
                        }
                    }
                    else
                    {
                        // qui attende meno...
                        DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec / 2);
                        if (iobObj.adpTryRestart && (DateTime.Now > dtVeto))
                        {
                            if (doLog)
                            {
                                lgInfo($"Retry Time Elapsed ({waitRecMSec / 2} ms)--> tryConnect");
                            }
                            lastStartTry = DateTime.Now;
                            iobObj.tryConnect();
                        }
                    }
                }
                else
                {
                    if (doLog)
                    {
                        lgInfo("Adapter stopped");
                    }
                    // verifico SE debba tentare il riavvio, ovvero NON running ma adpTryRestart
                    // e non ho riprovato x oltre waitRecMSec
                    DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec);
                    if (iobObj.adpTryRestart && (DateTime.Now > dtVeto))
                    {
                        lastStartTry = DateTime.Now;
                        AvviaAdapter(chkForceDequeue.Checked);
                    }
                }
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in fase di DoExecTasksAsync: {Environment.NewLine}{exc}");
            }
            finally
            {
                _plcLock.Release();
            }
        } 
#endif

        /// <summary>
        /// Ferma tutti i componenti adapter + update buttons
        /// </summary>
        /// <param name="stopTimer">
        /// indica se fermare il timer (gather) principale (solo se non si chiude)
        /// </param>
        /// <param name="tryRestart">indica se tentare di riconnettersi</param>
        /// <param name="forceDequeue">indica se sia richiesto di SVUOTARE le code delle info</param>
        /// <param name="updateForm">indica se si debba aggiornare la form (no se si sta chiudendo...)</param>
        private void fermaTutto(bool stopTimer, bool tryRestart, bool forceDequeue, bool updateForm)
        {
            if (iobObj != null)
            {
                try
                {
                    try
                    {
                        // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                        Task.Run(async () =>
                        {
                            await iobObj.stopAdapter(tryRestart, forceDequeue);
                            stop.Enabled = false;
                            start.Enabled = true;
                            restart.Enabled = false;
                            _isSuspended = true;
                            if (stopTimer)
                            {
                                gather.Enabled = false;
                                await StopWorker();
                            }
                        })
                            .GetAwaiter()
                            .GetResult();
                    }
                    catch (Exception ex)
                    {
                        lgError($"Errore in fermaTutto |stopTimer: {stopTimer} | tryRestart: {tryRestart} | forceDequeue: {forceDequeue} | updateForm: {updateForm}{Environment.NewLine}{ex.Message}");
                    }

                    newDisplayData currDispData = new newDisplayData();
                    currDispData.semIn = Semaforo.SS;
                    currDispData.semOut = Semaforo.SS;
                    if (updateForm)
                    {
                        updateFormDisplay(currDispData);
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Errore in chiusura:{Environment.NewLine}{exc}");
                }
            }
        }

        private void fixComboParameters()
        {
            if (iobObj != null)
            {
                if (iobObj.memMap != null)
                {
                    if (iobObj.memMap.mMapWrite != null)
                    {
                        if (iobObj.memMap.mMapWrite.Count > 0)
                        {
                            var oldParams = cmbParamValues.DataSource;
                            List<string> parametri = new List<string>();
                            foreach (var item in iobObj.memMap.mMapWrite)
                            {
                                parametri.Add(item.Key);
                            }
                            // salvo selezione
                            int oldIdx = cmbParamValues.SelectedIndex;
                            // riassegno e ri-seleziono
                            cmbParamValues.DataSource = parametri;
                            cmbParamValues.SelectedIndex = oldIdx >= 0 ? oldIdx : 0;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Evento principale scadenza del timer interno MsUI da cui scaturire le varie esecuzioni
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void gather_Tick(object sender, EventArgs e)
        {
            if (iobObj != null)
            {
                // fermo il timer...
                gather.Stop();
                try
                {
                    refreshFormData();
                }
                catch (Exception exc)
                {
                    lgError(string.Format("Eccezione in fase di gatherTick: {0}{1}", Environment.NewLine, exc));
                }

                // riavvio il timer...
                gather.Start();
            }
        }

        /// <summary>
        /// Init oggetti datamonitor
        /// </summary>
        private void initDatamonitor()
        {
            for (int i = 0; i < 4; i++)
            {
                dMonDisplVetoVeto.Add(i, DateTime.Now);
                dMonValues.Add(i, new List<string>());
            }
        }

        /// <summary>
        /// Carica file ini della configurazione richiesta
        /// </summary>
        /// <param name="iniConfFile"></param>
        private void loadIniFile(string iniConfFile)
        {
            // out di cosa faccio...
            displayTaskAndLog($"[STARTUP] Loading ConfFile: {iniConfFile}");
            var appVers = Assembly.GetExecutingAssembly().GetName().Version;
            string path = fileMover.GetExecutingDirectoryName(); // Directory.GetCurrentDirectory();
            string fileName = Path.GetFileName(iniConfFile).Replace(".ini", ".iob");
            string dirPath = Path.Combine(path, "DATA", "CONF_RUN");
            // verifica directory
            baseUtils.checkDir(dirPath);
            string fullPath = Path.Combine(dirPath, fileName);
            // preparazione file conf con nuovo formato e salvataggio...
            IOBConfFull = IobConfTree.LoadFromINI(iniConfFile);
            if (IOBConfFull.General.FileName.EndsWith("ini"))
            {
                IOBConfFull.General.FileName = IOBConfFull.General.FileName.Replace("ini", "yaml");
            }
            // salvo anche yaml...
            IOBConfFull.SaveYaml(fullPath.Replace("iob", "yaml"));

            // carico IOB
            _ = loadIobType();

            // verifico x invio
            string sendKey = "SendConfYaml";
            bool sendEnab = iobObj.CheckSendVeto(sendKey, TimeSpan.FromMinutes(120));
            // se abilitato faccio invio e salvo nuovo valore
            if (sendEnab)
            {
                // salvo nuovo valore invio
                iobObj.LastSendSet(sendKey, DateTime.Now);
                // invio file di conf!
                IOBConfFull.SendConfYaml(iobObj.urlSaveConfYaml);
            }

            // avvio macchina con adapter specificato...
            if (utils.CRB("autoStartOnLoad"))
            {
                displayTaskAndLog("Auto Starting...", true);
                // avvio!
                AvviaAdapter(chkForceDequeue.Checked);
                displayTaskAndLog("Auto Started!", true);
            }
        }

        /// <summary>
        /// Carica file yaml della configurazione richiesta
        /// </summary>
        /// <param name="yamlConfFile"></param>
        private void loadYamlFile(string yamlConfFile)
        {
            // out di cosa faccio...
            displayTaskAndLog($"[STARTUP] Loading yamlConfFile: {yamlConfFile}");
            // leggo file
            IniFile fIni = new IniFile(yamlConfFile);

            // carivo vettore parametri opzionai
            Dictionary<string, string> optParRead = new Dictionary<string, string>();
            string[] optParRows = fIni.ReadSection("OPTPAR");
            if (optParRows.Length > 0)
            {
                try
                {
                    string[] kvp;
                    foreach (var item in optParRows)
                    {
                        kvp = item.Split('=');
                        optParRead.Add(kvp[0], kvp[1]);
                    }
                    lgDebug($"Caricati {optParRead.Count} parametri opzionali da OPTPAR");
                }
                catch (Exception exc)
                {
                    lgError(string.Format("EXCEPTION in fase di lettura OPTPAR: {0}{1}", Environment.NewLine, exc));
                }
            }
            var appVers = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;

            _ = loadIobType();
            // avvio macchina con adapter specificato...
            if (utils.CRB("autoStartOnLoad"))
            {
                displayTaskAndLog("Auto Starting...", true);
                // avvio!
                AvviaAdapter(chkForceDequeue.Checked);
                displayTaskAndLog("Auto Started!", true);
            }
        }

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

            return doLog;
        }

        /// <summary>
        /// Fase chiusura Form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            lgTrace("MainForm_FormClosing");
            closeAdapter();
        }

        /// <summary>
        /// Completato resize form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Resize(object sender, EventArgs e)
        {
            // Verifichiamo se la finestra è appena stata ripristinata
            if (this.WindowState != FormWindowState.Minimized && _lastState == FormWindowState.Minimized)
            {
                displayTaskAndLog("Adapter Form Restored from Minimized");
            }
            _lastState = this.WindowState;
        }

        /// <summary>
        /// Mostrata form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Shown(object sender, EventArgs e)
        {
            displayTaskAndLog("Adapter Form SHOWN (Adapter)", true);
        }

        private void mLoadConf_Click(object sender, EventArgs e)
        {
            // mostro selettore file x leggere adapter..
            OpenFileDialog openFileDial = new OpenFileDialog();

            // directory iniziale
            openFileDial.InitialDirectory = utils.confDir; // string.Format(@"{0}\{1}", Application.StartupPath, utils.CRS("dataConfPath"));
                                                           // Set filter options and filter index.
            openFileDial.Filter = "INI Files (.ini)|*.ini|All Files (*.*)|*.*";
            openFileDial.FilterIndex = 1;
            // altre opzioni
            openFileDial.Multiselect = false;

            // Call the ShowDialog method to show the dialog box.
            DialogResult userClickedOK = openFileDial.ShowDialog();

            // Process input if the user clicked OK.
            if (userClickedOK == DialogResult.OK)
            {
                string iniConfFile = openFileDial.FileName;
                loadIniFile(iniConfFile);
                lgInfo("INI LOADED");
            }
        }

        private void refreshFormData()
        {
            // aggiorno visualizzazioni varie in form...
            alQueueLen = iobObj.QueueAlarm.Count;
            evQueueLen = iobObj.QueueIN.Count;
            flQueueLen = iobObj.QueueFLog.Count;
            msQueueLen = iobObj.QueueMessages.Count;
            rtrQueueLen = iobObj.QueueRawTransf.Count;
            ulQueueLen = iobObj.QueueULog.Count;
            // aggiorno labels counters...
            counterIob = $"pz MES {iobObj.contapezziIOB}";
            counterMac = $"pz PLC {iobObj.contapezziPLC}";
            Dictionary<string, string> setPar = new Dictionary<string, string>();
            setPar.Add("IP", iobObj.IOBConfFull.Device.Connect.IpAddr);
            setPar.Add("PORT", iobObj.IOBConfFull.Device.Connect.Port);
            string note = $"{iobObj.IOBConfFull.General.FilenameIOB} | DT ultimo avvio: {iobObj.dtAvvioAdp}";
            // verifico IOB status
            IobWinStatus currIobStatus = new IobWinStatus()
            {
                CodIob = iobObj.IOBConfFull.General.FilenameIOB,
                IobType = $"{iobObj.IOBConfFull.General.IobType}",//iobObj.cIobConf.tipoIob.ToString(),
                queueAlLen = alQueueLen,
                queueEvLen = evQueueLen,
                queueFlLen = flQueueLen,
                queueMsLen = msQueueLen,
                queueRawTransfLen = rtrQueueLen,
                queueUlLen = ulQueueLen,
                counterIOB = iobObj.contapezziIOB,
                counterMAC = iobObj.contapezziPLC,
                lastUpdate = lastIobStatus.lastUpdate > iobObj.lastWatchDog ? lastIobStatus.lastUpdate : iobObj.lastWatchDog,
                online = utils.IOB_Online,
                lastDataIn = iobObj.lastReadPLC,
                lastDataOut = iobObj.lastIobOnline,
                setupParams = setPar,
                freeNotes = note
            };
            // se diverso SALVO!
            if (!currIobStatus.Equals(lastIobStatus))
            {
                // aggiorno data
                currIobStatus.lastUpdate = DateTime.Now;
                // salvo su redis e in obj corrente
                iobObj.redisMan.iobStatus = currIobStatus;
                lastIobStatus = currIobStatus;
            }
            // se diverso SALVO MP IO status
            if (lastSrvStatus.online != utils.MPIO_Online)
            {
                // aggiorno
                ServerMpStatus currSrvStatus = iobObj.redisMan.servStatus;
                currSrvStatus.online = utils.MPIO_Online;
                currSrvStatus.lastUpdate = DateTime.Now;
                // salvo su redis e in obj corrente
                iobObj.redisMan.servStatus = currSrvStatus;
                lastSrvStatus = currSrvStatus;
            }
        }

        /// <summary>
        /// Button restart
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void restart_Click(object sender, EventArgs e)
        {
            string message = "Attenzione: con l'operazione di reset veranno persi (e non inviati) i dati eventualmente accumulati (indipendentemente dalla selezione del checkbox 'svuota coda').";
            string caption = "Conferma Reset Dati IOB";
            MessageBoxButtons buttons = MessageBoxButtons.YesNo;
            DialogResult result;

            // verifica con messagebox
            result = MessageBox.Show(message, caption, buttons);
            // solo se ho conferma da utente
            if (result == DialogResult.Yes)
            {
                // faccio stop... SENZA inviare
                fermaAdapter(false, false, true);
                displayTaskAndLog("RESTARTING: Adapter Stopped", true);
                // rileggo INI
                loadIniFile(defConfFilePath);
                displayTaskAndLog("RESTARTING: Ini File Reloaded", true);
                // faccio start... CON RESET delle code
                AvviaAdapter(true);
                displayTaskAndLog("RESTARTING: Adapter Started", true);
                // resetto i data monitor...
                dataMonitor_0 = "";
                dataMonitor_1 = "";
                dataMonitor_2 = "";
                dataMonitor_3 = "";
            }
        }

        /// <summary>
        /// Invio per backup contenuto file conf
        /// </summary>
        /// <param name="confFilePath"></param>
        private async Task sendConfFile(string confFilePath)
        {
            string answ = "";
            fileEmbed objFiles = new fileEmbed();
            try
            {
                if (File.Exists(confFilePath))
                {
                    string fileContent = System.IO.File.ReadAllText($"{confFilePath}");
                    smallFile currFile = new smallFile()
                    {
                        fileName = confFilePath,
                        content = fileContent.Replace("\r\n", Environment.NewLine)
                    };
                    objFiles.fileList.Add(currFile);
                    // serializzo
                    string rawData = JsonConvert.SerializeObject(objFiles);
                    // mod 2022.12.30: verifico se diverso da copia redis ultimo MD5 del set
                    // salvato, nel caso NON invio...
                    bool needSend = false;
                    using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
                    {
                        var hash = md5.ComputeHash(Encoding.ASCII.GetBytes(rawData));
                        string newMD5 = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
                        string redKey = iobObj.redisMan.redHash($"IOB:ConfFileMD5:{CurrIOB}");
                        var rawVal = iobObj.redisMan.getRSV(redKey);
                        // verifico uguaglianza MD5...
                        if (string.IsNullOrEmpty(rawVal) || !newMD5.Equals(rawVal))
                        {
                            needSend = true;
                            int CacheConfToCloudDuratHour = utils.CRI("CacheConfToCloudDuratHour");
                            // calcolo attesa con errore random -/+ 10%
                            int ttlSec = 60 * 60 * CacheConfToCloudDuratHour * rndGen.Next(900, 1100) / 1000;
                            iobObj.redisMan.setRSV(redKey, newMD5, ttlSec);
                        }
                    }
                    // invio solo se necessario
                    if (needSend)
                    {
                        if (utils.CRB("ConfToCloud"))
                        {
                            // invio su cloud...
                            answ = await utils.callUrlAsync($"{urlUploadFileCloud}{CurrIOB}", rawData);
                        }
                        else
                        {
                            // provo invio locale
                            answ = utils.callUrl($"{urlUploadFile}{CurrIOB}", rawData);
                            // se va male invio cloud...
                            if (answ.ToUpper() != "OK")
                            {
                                answ = await utils.callUrlAsync($"{urlUploadFileCloud}{CurrIOB}", rawData);
                            }
                        }
                    }
                }
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in upload IOB conf files{Environment.NewLine}{exc}");
            }
        }

        /// <summary>
        /// Indica se debba aggiornare o meno la UI perché minimizzata o meno
        /// </summary>
        /// <returns></returns>
        private bool ShouldUpdateUI()
        {
            // 1. Controlla se la form stessa è minimizzata
            if (this.WindowState == FormWindowState.Minimized) return false;

            // 2. Se è una MDI Child, controlla se il Padre è minimizzato
            if (this.MdiParent != null && this.MdiParent.WindowState == FormWindowState.Minimized)
                return false;

            // 3. (Opzionale) Controlla se la form è visibile
            if (!this.Visible) return false;

            return true;
        }

        private void splitContainer1_Panel1_Paint(object sender, PaintEventArgs e)
        {
        }

        /// <summary>
        /// Avvio dell'adapter
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void start_Click(object sender, EventArgs e)
        {
            stopForced = false;
            AvviaAdapter(chkForceDequeue.Checked);
            // salvo che ho avviato adapter
            lgInfo("Completato LOAD Adapter");
        }

        /// <summary>
        /// Metodo per avviare il worker (es. nel Form_Load)
        /// </summary>
        private void StartWorker()
        {

            //// FixMe ToDo !!! togliere o fare alternativa pooled
            //if (iobObj.IOBConfFull.General.MachWLoopSingleThread || true)
            //{
            //}
            lock (_threadLock)
            {
                // rimozione isSuspended
                _isSuspended = false;

                // 1. Ferma eventuali worker precedenti
                if (_ctsMachine != null)
                {
                    _ctsMachine.Cancel();
                    _ctsMachine.Dispose(); // Libera le risorse del vecchio token
                }

                // 2. Attendi che il thread precedente sia effettivamente uscito
                if (_machineThread != null && _machineThread.IsAlive)
                {
                    // Timeout breve per non freezare la UI, ma necessario per pulizia
                    _machineThread.Join(1000);
                }

                _ctsMachine = new CancellationTokenSource();

                // 3. Crea un thread NUOVO e DEDICATO
                _machineThread = new Thread(() => WorkerLoopMachine(_ctsMachine.Token))
                {
                    IsBackground = true,
                    Name = "Single_Dedicated_Thread",
                    Priority = ThreadPriority.AboveNormal // Opzionale: dà precedenza ai dati PLC
                };

                _machineThread.Start();
                lgInfo("--- WORKER MACHINE AVVIATO ---");

                // avvio altro thread comunque...
                // 1. Controllo se è già in esecuzione
                if (_workerTask != null && !_workerTask.IsCompleted)
                {
                    lgTrace("Worker già in esecuzione. Richiesta ignorata.");
                    return;
                }

                // avvio componente multithreaded
                _ctsServer = new CancellationTokenSource();
                _workerTask = Task.Run(() => WorkerLoopAsync(_ctsServer.Token));
                lgInfo("--- WORKER SERVER AVVIATO ---");
            }


        }


        private Thread _machineThread;
        private readonly object _threadLock = new object();


#if false
        private BlockingCollection<Action> _workQueue = new BlockingCollection<Action>(); 
#endif

        /// <summary>
        /// fermata dell'adapter
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void stop_Click(object sender, EventArgs e)
        {
            stopForced = true;
            fermaAdapter(false, chkForceDequeue.Checked, true);
            // salvo che ho fermato adapter
            lgInfo("UNLOAD Adapter");
        }

        // Metodo per fermare tutto (es. nel Form_Closing o Dispose)
        private async Task StopWorker()
        {
            if (_ctsMachine == null) return;

            _ctsMachine.Cancel();

            try
            {
                // Opzionale: attendi che il task finisca davvero prima di fare il dispose
                if (_workerTask != null)
                {
                    await _workerTask;
                }
            }
            catch (OperationCanceledException) { /* Normale amministrazione */ }
            finally
            {
                if (_ctsMachine != null)
                {
                    _ctsMachine.Dispose();
                    _ctsMachine = null;
                }
                if (_workerTask != null)
                {
                    _workerTask = null;
                }
            }
        }

        private void TabData_Selected(object sender, TabControlEventArgs e)
        {
        }

        private void toggleEditMes2Plc()
        {
            // abilita i campi --> PLC per editing
            enableEditMes2Plc = !enableEditMes2Plc;
            checkEditMes2Plc();
        }

        private void txtMReadSize_TextChanged(object sender, EventArgs e)
        {
            checkAssignSize();
        }

        private void txtMReadStart_TextChanged(object sender, EventArgs e)
        {
            checkAssignStart();
            checkAssignSize();
        }

        /// <summary>
        /// Counter esecuzione WorkerLoopMachine
        /// </summary>
        private int idxWLM = 0;
        /// <summary>
        /// Counter esecuzione WorkerLoopServer
        /// </summary>
        private int idxWLS = 0;

        /// <summary>
        /// Loop di gestione worker comunicazione con la macchina (single threaded)
        /// </summary>
        /// <param name="ct"></param>
        private void WorkerLoopMachine(CancellationToken ct)
        {
            lgInfo($"[Thread {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] inizio WorkerLoopMachine");
            int validThread = Thread.CurrentThread.ManagedThreadId;
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    // incremento indice x mostrare esecuzione...
                    idxWLM++;
                    idxWLM = idxWLM > 999 ? 0 : idxWLM;
                    //lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] WorkerLoopMachine");
                    if (!_isSuspended)
                    {
                        try
                        {
                            // solo se il thread è valido...
                            if (Thread.CurrentThread.ManagedThreadId == validThread)
                            {
                                // Eseguiamo il lavoro asincrono in modo SINCRONO su questo thread.
                                // .GetAwaiter().GetResult() blocca questo thread finché il task non è finito.
                                //await DoExecMachineTasks();
                                //DoExecMachineTasks().GetAwaiter().GetResult();
                                DoExecMachineTasks();
                            }
                            else
                            {
                                lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] | WorkerLoopMachine | NOT VALID, expecting {validThread}");
                            }
                        }
                        catch (Exception ex)
                        {
                            lgError($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] Errore nel task: {ex.Message}");
                        }
                    }
                    else
                    {
                        lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] WorkerLoopMachine suspended");
                        // se non fosse stop richiesto da utente...
                        if (!stopForced)
                        {
                            DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec / 2);
                            // verifica scadenza controllo SE fosse offline x eseguire test riconnessione...
                            if (iobObj.adpTryRestart && (DateTime.Now > dtVeto) && iobObj.adpRunning && !iobObj.connectionOk)
                            {
                                lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] WorkerLoopMachine sospeso: tentativo riavvio periodico");
                                lastStartTry = DateTime.Now;
                                iobObj.tryConnect();
                            }
                            else
                            {
                                lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] WorkerLoopMachine sospeso: NON esegue task specifici | _isSuspended: {_isSuspended}");
                            }
                        }
                    }

                    // Calcolo del delay dinamico tra standard e suspended da Timers.MsVHF
                    int currentDelay = _isSuspended ? 10 * IOBConfFull.General.Timers.MsVHF : IOBConfFull.General.Timers.MsVHF;

                    // ATTESA: Non rilascia il thread al pool di .NET
                    // Si interrompe subito se ct viene cancellato
                    bool cancelled = ct.WaitHandle.WaitOne(currentDelay);
                    if (cancelled) break;

                    //lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] WorkerLoopMachine | End");
                }
            }
            catch (Exception ex)
            {
                lgError($"FATAL: Il thread dedicato {Thread.CurrentThread.ManagedThreadId} ({idxWLM}) è terminato: {ex.Message}");
            }
            finally
            {
                lgInfo($"[Thread {Thread.CurrentThread.ManagedThreadId} ({idxWLM})] WorkerLoopMachine terminato.");
            }
        }

        /// <summary>
        /// Loop di gestione worker
        /// </summary>
        /// <param name="ct"></param>
        /// <returns></returns>
        private async Task WorkerLoopAsync(CancellationToken ct)
        {
            try
            {
                lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLS})] Inizio WorkerLoopAsync");

                while (!ct.IsCancellationRequested)
                {
                    // incremento indice x mostrare esecuzione...
                    idxWLS++;
                    idxWLS = idxWLS > 999 ? 0 : idxWLS;
                    //lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLS})] WorkerLoopAsync");
                    if (!_isSuspended)
                    {
                        try
                        {
                            await DoExecServerTasksAsync(false);

                        }
                        catch (Exception ex)
                        {
                            lgError($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLS})] Errore durante i task: " + ex.Message);
                        }
                    }
                    else
                    {
                        lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLS})] WorkerLoopAsync suspended");
                        // se non fosse stop richiesto da utente...
                        if (!stopForced)
                        {
                            DateTime dtVeto = lastStartTry.AddMilliseconds(waitRecMSec / 2);
                            // verifica scadenza controllo SE fosse offline x eseguire test riconnessione...
                            if (iobObj.adpTryRestart && (DateTime.Now > dtVeto) && iobObj.adpRunning && !iobObj.connectionOk)
                            {
                                lgTrace($"WorkerLoopAsync sospeso: tentativo riavvio periodico");
                                lastStartTry = DateTime.Now;
                                iobObj.tryConnect();
                            }
                            else
                            {
                                lgTrace($"WorkerLoopAsync sospeso: NON esegue task specifici | _isSuspended: {_isSuspended}");
                            }
                        }
                    }

                    // Calcolo del delay dinamico tra standard e suspended da Timers.MsVHF
                    int currentDelay = _isSuspended ? 10 * IOBConfFull.General.Timers.MsVHF : IOBConfFull.General.Timers.MsVHF;

                    // se ho error delay --> log!
                    if (_errorDelay > 0)
                    {
                        currentDelay += _errorDelay;
                        lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLS})] WorkerLoopAsync | delay: {currentDelay}");
                    }

                    // 4. ATTESA INTERROMPIBILE
                    // Il delay asincrono permette al sistema di riutilizzare il thread se necessario,
                    // ma il 'while' garantisce che non inizieremo il prossimo DoExecTasksAsync prematuramente.
                    //await Task.Delay(currentDelay, ct).ConfigureAwait(false);
                    await Task.Delay(currentDelay, ct);

                    // Logga l'ID Thread
                    //lgTrace($"[Thread: {Thread.CurrentThread.ManagedThreadId} ({idxWLS})] WorkerLoopAsync | End");
                }
                lgInfo("WorkerLoopAsync interrotto per cancellation token.");
            }
            catch (OperationCanceledException)
            {
                // Eccezione normale quando si preme Stop/Cancel
                lgInfo("WorkerLoopAsync interrotto correttamente.");
            }
        }

        #endregion Private Methods
    }
}