using IOB_UT_NEXT;
using IOB_UT_NEXT.Config;
using MapoSDK;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.AccessControl;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static IOB_UT_NEXT.CustomObj;

namespace IOB_WIN_FORM.Iob
{
    /// <summary>
    /// Configuraizone eventi da simulare
    /// </summary>
    public class simPar
    {
        #region Public Fields

        /// <summary>
        /// Durata dell'evento
        /// </summary>
        public int duration = 1;

        /// <summary>
        /// DateTime ultimo evento
        /// </summary>
        public DateTime lastEv = DateTime.Now;

        /// <summary>
        /// Attesa per evento
        /// </summary>
        public int wait = 10;

        #endregion Public Fields
    }

    public class Simula : Generic
    {
        #region Public Fields

        /// <summary>
        /// Ora spegniemnto (standard)
        /// </summary>
        public int tOff = 22;

        /// <summary>
        /// Ora dia ccensione (standard)
        /// </summary>
        public int tOn = 6;

        #endregion Public Fields

        #region Public Constructors

        /// <summary>
        /// estende l'init della classe base...
        /// </summary>
        /// <param name="caller">Form chiamante</param>
        /// <param name="IobConfFull">Configurazione (v 4.x)</param>
        public Simula(AdapterForm caller, IobConfTree IobConfFull) : base(caller, IobConfFull)
        {
            // gestione invio ritardato contapezzi
            DateTime adesso = DateTime.Now;
            lastPzCountSend = adesso;
            lastWarnODL = adesso;
            lastEvCheck = adesso;
            lastSimData = adesso;
            lastRedDuration = adesso;
            // abilito subito scrittura coda IN
            queueInEnabCurr = true;
            // setup allarmi simulati
            for (int i = 1; i <= 8; i++)
            {
                alarmMessages.Add($"Allarme {i:000}");
            }
            // sistemo parametri x simulazione...
            if (IobConfFull.OptPar.Count > 0)
            {
                if (!string.IsNullOrEmpty(IOBConfFull.OptParGet("PER_BASE")))
                {
                    int.TryParse(IOBConfFull.OptParGet("PER_BASE"), out periodoMSec);
                    // aggiungo NOISE... +/- 10%
                    Random rnd = new Random();
                    int noise = rnd.Next(1, periodoMSec / 5);
                    periodoMSec += noise - (periodoMSec / 10);
                }
                bool.TryParse(IOBConfFull.OptParGet("SIM_POW_ON_OFF"), out simPowerOnOff);
                bool.TryParse(IOBConfFull.OptParGet("SIM_IS_SLAVE"), out simSlave);
                int.TryParse(IOBConfFull.OptParGet("T_ON"), out tOn);
                int.TryParse(IOBConfFull.OptParGet("T_OFF"), out tOff);
                bit2 = setupSimPar("SIM_PZCNT");
                bit3 = setupSimPar("SIM_ALARM");
                bit4 = setupSimPar("SIM_MANU");
                bit5 = setupSimPar("SIM_SLOW");
                bit6 = setupSimPar("SIM_WUCD");
                bit7 = setupSimPar("SIM_EMRG");
                int.TryParse(IOBConfFull.OptParGet("MIN_DURATA_ODL"), out minDurataODL);
                var strDisableSim = IOBConfFull.OptParGet("DISABLE_SIM_STATUS");
                if (!string.IsNullOrEmpty(strDisableSim))
                {
                    bool.TryParse(strDisableSim, out disableSimStatus);
                }
                // simulazioni azioni utente
                simRC = setupSimPar("SIM_RC");
                simRS = setupSimPar("SIM_RS");
                simDich = setupSimPar("SIM_DICH");
                int.TryParse(IOBConfFull.OptParGet("SIM_MATR_OPR"), out matrOpr);
            }

            string sFluxOnRead = IOBConfFull.OptParGet("sendFluxOnRead");
            if (!string.IsNullOrEmpty("sendFluxOnRead"))
            {
                bool.TryParse(sFluxOnRead, out sendFluxOnRead);
            }

            // riesegue setup PLC
            setParamPlc();
            // ricarico da server i dati dei pezzi fatti...
            lgInfo("Init contapezzi SIMULA: pzCntReload(true)");
            if (!isMulti)
            {
                pzCntReload(true);
            }
            // imposto pezzi CNC ai pezzi contati da server...
            contapezziPLC = contapezziIOB;
            lgInfo($"Impostazione iniziale contatori: contapezzi macchina contapezziPLC: {contapezziPLC} | contapezziIOB: {contapezziIOB}");

            // parto processando dynData
            processDynData();

            if (!string.IsNullOrEmpty(IOBConfFull.OptParGet("FTP_AT_START")) || !string.IsNullOrEmpty(IOBConfFull.OptParGet("DATA_SYNC_AT_START")))
            {
                // simulazione processo FTP (Cimolai) - FIXME TODO DeleteME
                ProcessDataSync();
            }
            // carica conf
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    await Simula_Load(adesso);
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in Simula_Load{Environment.NewLine}{ex.Message}");
            }
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// Processo i task richiesti e li elimino dalla coda 1:1 (in realtà SOLO forceSetPzCount x ora)
        /// </summary>
        /// <param name="task2exe"></param>
        public override Dictionary<string, string> executeTasks(Dictionary<string, string> task2exe, string codTav)
        {
            // Verificare il protocollo: dovrebeb togliere SOLO i task eseguiti...
            Dictionary<string, string> taskDone = new Dictionary<string, string>();
            // controlo SE sia disabilitata simulazione principale
            if (!disableSimStatus)
            {
                string taskVal = "";
                string newVal = "";
                // verifico non sia null
                if (task2exe != null)
                {
                    // cerco task specifici
                    foreach (var item in task2exe)
                    {
                        taskVal = "";
                        newVal = "";
                        // converto richiesta in enum...
                        taskType tName = taskType.nihil;
                        Enum.TryParse(item.Key, out tName);
                        // controllo sulla KEY
                        switch (tName)
                        {
                            case taskType.setArt:
                            case taskType.setComm:
                            case taskType.setProg:
                            case taskType.setPzComm:
                                // da memMap...
                                if (memMap != null && memMap.mMapWrite != null)
                                {
                                    if (memMap.mMapWrite.ContainsKey(item.Key))
                                    {
                                        dataConf currMem = memMap.mMapWrite[item.Key];
                                        string addr = currMem.memAddr;
                                        taskVal = $"SET task: {item.Key} --> {item.Value} | mem: {currMem.memAddr} - {currMem.size} byte";
                                        // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà
                                        memMap.mMapWrite[item.Key].value = item.Value;
                                    }
                                    else
                                    {
                                        taskVal = $"NO DATA MEM, SET task: {item.Key} --> {item.Value} ({item.Value})";
                                    }
                                }
                                else
                                {
                                    taskVal = $"NO BankConf found, SET task: {item.Key} --> {item.Value}";
                                }
                                // salvo in currProd..
                                upsertKey(item.Key, item.Value);

                                // aggiunto x debug, forse possibile eliminare IN TOTO (da FANUC e Mitsubishi...)
                                _ = sendOptVal(item.Key, newVal);

                                break;

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

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

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

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

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

                            case taskType.nihil:
                            case taskType.sendWatchDogMes2Plc:
                            case taskType.startSetup:
                            case taskType.stopSetup:
                                taskVal = $"taskReq: {tName} | key: {item.Key} | val: {item.Value} | SKIPPED | NO EXEC";
                                break;

                            case taskType.fixStopSetup:
                                // ricevuto ad es forzatamente x macchine SLAVE, serve x fermare simulazione  contapezzi
                                if (simSlave)
                                {
                                    contapezziPLC = 0;
                                    taskVal = "SLAVE: fixStopSetup --> RESET contapezziPLC";
                                }
                                break;

                            case taskType.forceResetPzCount:
                                // forzo sul SIM il valore a ZERO x l'IOB...
                                contapezziPLC = 0;
                                taskVal = "RESET contapezziPLC: 0";
                                break;

                            case taskType.forceSetPzCount:
                                // forzo sul SIM il valore pzCount dell'IOB...
                                int newPzCount = contapezziPLC;
                                bool fatto = int.TryParse(item.Value, out newPzCount);
                                if (fatto)
                                {
                                    // forzo update
                                    contapezziPLC = newPzCount;
                                    taskVal = $"Set new contapezziPLC: {contapezziPLC}";
                                }
                                break;

                            case taskType.syncDbData:
                                ProcessDataSync();
                                lgInfo("Richiesta syncDbData");
                                break;

                            default:
                                taskVal = "SKIPPED | NO EXEC";
                                break;
                        }
                        // aggiungo task!
                        taskDone.Add(item.Key, taskVal);

                        // salvo SIMULAZIONE dati scambiati: lunghezza string
                        trackExchData(taskVal.Length, 1792);
                    }
                }
            }
            return taskDone;
        }

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

        /// <summary>
        /// Recupero dati dinamici...
        /// </summary>
        public override Dictionary<string, string> getDynData()
        {
            bool useLUT = true;
            // valore non presente in vers default... se gestito fare override
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            // verificare periodo SIM parametri... se passato li invio altrimenti NO... FIX a 20 sec
            if (lastSimData.AddSeconds(waitSimPar) < DateTime.Now && memMap != null)
            {
                Random rnd = new Random();
                // controllo conf memorie json (se ci sono...)
                try
                {
                    if (memMap.mMapWrite.Count > 0)
                    {
                        foreach (var item in memMap.mMapWrite)
                        {
                            if (useLUT)
                            {
                                saveValueString(ref outVal, item.Key, item.Value.value);
                            }
                            else
                            {
                                outVal.Add(item.Key, item.Value.value);
                            }
                        }
                    }
                    if (memMap.mMapRead.Count > 0)
                    {
                        foreach (var item in memMap.mMapRead)
                        {
                            // se il TIPO di valore è livello --> simulo variazione da MAX --> min,
                            // con un delta in CALO pari a factor * (80-120)%
                            if (item.Value.name == "SIM_LEVEL")
                            {
                                // verifico last value
                                double lastVal = 0;
                                double.TryParse(item.Value.value, out lastVal);
                                if (lastVal == 0)
                                {
                                    lastVal = item.Value.maxVal - (double)item.Value.factor;
                                }
                                // decremento casuale...
                                double newVal = lastVal - ((double)item.Value.factor * rnd.Next(40, 120) / 100);
                                // se inferiore a minimo --> massimo!
                                if (newVal < item.Value.minVal)
                                {
                                    newVal = item.Value.maxVal;
                                }
                                // salvo il suo VALUE...
                                item.Value.value = $"{newVal}";
                                if (useLUT)
                                {
                                    saveValue(ref outVal, item.Key, newVal);
                                }
                                else
                                {
                                    outVal.Add(item.Key, $"{newVal}");
                                }
                            }
                            // altrimenti simulazione random walk...
                            else
                            {
                                double randVal = 0;
                                if (item.Value.factor == 1)
                                {
                                    // uso factor come valore MAX ammesso
                                    randVal = rnd.Next((int)item.Value.minVal, (int)item.Value.maxVal);
                                }
                                else
                                {
                                    // uso factor come fattore di divisione x simulare decimali
                                    randVal = ((float)rnd.Next((int)item.Value.minVal, (int)item.Value.maxVal)) / (float)item.Value.factor;
                                }
                                // verifico uso LUT
                                if (useLUT)
                                {
                                    saveValue(ref outVal, item.Key, randVal);
                                }
                                else
                                {
                                    outVal.Add(item.Key, $"{randVal}");
                                }
                            }
                        }
                    }
                }
                catch
                { }
                lastSimData = DateTime.Now;
            }
            return outVal;
        }

        /// <summary>
        /// Recupero programma in lavorazione
        /// </summary>
        /// <returns></returns>
        public override string getPrgName()
        {
            // NOME DEL SIM!
            string prgName = $"PROG_{IOBConfFull.General.CodIOB}";
            return prgName;
        }

        /// <summary>
        /// Recupero info sistema generiche
        /// </summary>
        /// <returns></returns>
        public override Dictionary<string, string> getSysInfo()
        {
            // valore non presente in vers default... se gestito fare override
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            outVal.Add("MACHINE", "IOB_SIM");
            return outVal;
        }

        /// <summary>
        /// Caricamento conf specifica in async
        /// </summary>
        /// <returns></returns>
        public override async Task InitializeAsync()
        {
            await Simula_Load(DateTime.Now);
            // attendo altri 50ms
            await Task.Delay(50);
        }

        /// <summary>
        /// Effettua vero processing contapezzi
        /// </summary>
        public override void processContapezzi()
        {
        }

        /// <summary>
        /// Effettua processing CUSTOM
        /// </summary>
        public override void processCustomTaskLF()
        {
            lgDebug($"Richiesto processCustomTaskLF");
            DateTime adesso = DateTime.Now;

            // invio dataItems come MTC
            List<machDataItem> demoItems = new List<machDataItem>();
            demoItems.Add(new machDataItem() { uuid = "AVAIL", Category = DataItemCategory.EVENT, Type = "AVAILABILITY" });
            demoItems.Add(new machDataItem() { uuid = "ACC_TIME", Category = DataItemCategory.SAMPLE, Type = "ACCUMULATED_TIME", Name = "[s] Tempo macchina in potenza" });
            demoItems.Add(new machDataItem() { uuid = "CLOCK", Category = DataItemCategory.SAMPLE, Type = "CLOCK_TIME" });
            sendDataItemsList(demoItems);

            // salvo lettura
            lastReadPLC = adesso;
            lgInfo($"Completato processCustomTaskLF");
        }

        /// <summary>
        /// Effettua processing CUSTOM
        /// </summary>
        public override void processCustomTaskMF()
        {
            lgDebug($"Richiesto processCustomTaskMF");
            DateTime adesso = DateTime.Now;

            // effettua vari processing custom
            pzCntReload(true);
            // effettua vari processing custom
            var currOdl = CurrOdl();

            // salvo lettura
            lastReadPLC = adesso;
            lgInfo($"Completato processCustomTaskMF");
        }

        /// <summary>
        /// Processo contatori eventi...
        /// </summary>
        public override void processVHF()
        {
            if (lastEvCheck.AddMilliseconds(periodoMSec) < DateTime.Now)
            {
                // decremento contatore ultimo evento
                if (bit2 != null)
                    bit2.wait--;

                if (bit3 != null)
                    bit3.wait--;

                if (bit4 != null)
                    bit4.wait--;

                if (bit5 != null)
                    bit5.wait--;

                if (simDich != null)
                    simDich.wait--;

                if (simRC != null)
                    simRC.wait--;

                if (simRS != null)
                    simRS.wait--;
                lastEvCheck = DateTime.Now;
            }
        }

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

            // controlo SE sia disabilitata simulazione principale
            if (!disableSimStatus)
            {
                // decodifica e gestione
                decodeToBaseBitmap();
                decodeOtherData();
                reportRawInput(ref currDispData);
            }
        }

        /// <summary>
        /// Effettua reset del contapezzi
        /// </summary>
        /// <returns></returns>
        public override bool resetContapezziPLC(string codTav)
        {
            lgInfo("OpcUa.resetContapezziPLC - mockup true");
            // indico fatto...
            return true;
        }

        public async Task Simula_Load(DateTime adesso)
        {
            // test ricetta
            if (hasRecipe)
            {
                if (pathList.Count > 0)
                {
                    Calendar cal = new CultureInfo("it-IT").Calendar;
                    int week = cal.GetWeekOfYear(adesso, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
                    string tempPath = Path.Combine(pathList["path-locBase"], pathList["path-01-Temp"]);
                    string archPath = Path.Combine(pathList["path-locBase"], pathList["path-02-Sent"], $"{adesso:yyyy}", $"{week:00}");
                    string remoPath = Path.Combine(pathList["path-locBase"], pathList["path-04-remReq"]);
                    baseUtils.checkDir(tempPath);
                    baseUtils.checkDir(archPath);
                    try
                    {
                        // recupero elenco PODL da processare, check PODL già inviati, save locale
                        bool create = RecipeReqWriteLocal(tempPath, useLocalRecipe);
                        // invio ricette a impianto
                        bool trasmitted = RecipeSend(tempPath, archPath, remoPath);
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in TEST ProcessDataSync{Environment.NewLine}{exc}");
                    }
                }
                else
                {
                    lgWarn($"Manca conf pathList, impossiibile simulare gestione Recipe");
                }
            }

            // test gestione fileImport...
            if (!string.IsNullOrEmpty(fileImportFolder))
            {
                await ProcessFileImportAsync();
            }
            if (hasRecipe)
            {
                try
                {
                    // effettua process ritorno ricette
                    await ProcessRecipeFileRetAsync();
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in TEST ProcessRecipeFileRetAsync{Environment.NewLine}{exc}");
                }
                try
                {
                    // eseguo verifica/lancio ricette
                    RecipeDoProcCons();
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in TEST RecipeDoProcCons{Environment.NewLine}{exc}");
                }
            }
            // commentato post test x Soitaab
            if (EnabelPodlManFull)
            {
                // test decodifica log (tipo soitaab)
                try
                {
                    // eseguo subito un ciclo acquisizione log + processing
                    bool acquireLog = getRemoteLog();
                    if (acquireLog)
                    {
                        bool sentSignLog = ProcessSignLogTable(adesso);
                        bool sentFluxLog = ProcessFluxLogTable();
                    }
                }
                catch (Exception exc)
                {
                    lgError($"Eccezione in EnabelPodlManFull{Environment.NewLine}{exc}");
                }
            }
        }

        public override void tryConnect()
        {
            base.tryConnect();
            connectionOk = true;
        }

        public override void tryDisconnect()
        {
            base.tryDisconnect();
            connectionOk = false;
        }

        /// <summary>
        /// Formatta URL x invio in DataBlock Json dei dati FLog / eventi OVERRIDE x invio come tav
        /// 1/2 per USER LOG....
        /// </summary>
        /// <param name="tipoUrl"></param>
        /// <returns></returns>
        public override string urlDataBlock(urlType tipoUrl)
        {
            // recupero tipo base
            string answ = base.urlDataBlock(tipoUrl);
            // se fosse una macchina doppio pallet...
            if (isMulti)
            {
                // SOLO x la parte USER LOG
                switch (tipoUrl)
                {
                    case urlType.ULog:
                        string[] elencoMulti = null;
#if false
                        // devo chiamare cambio ODL x OGNI tavola: mi servono i parametri opzionali...
                        elencoMulti = IOBConfFull.Device.MultiIobListRaw.Split(',');
#endif
                        elencoMulti = IOBConfFull.Device.MultiIobList.ToArray();
                        // aggiungo tav 1 / 2 a caso secondo secondi attuali, se pari/dispari
                        int idxTav = DateTime.Now.Second % 2;
                        answ += $"#{elencoMulti[idxTav]}";
                        break;

                    default:
                        break;
                }
            }
            return answ;
        }

        #endregion Public Methods

        #region Protected Fields

        /// <summary>
        /// Parametri simulazione oscillazione bit 2
        /// </summary>
        protected simPar bit2;

        /// <summary>
        /// Parametri simulazione oscillazione bit 3
        /// </summary>
        protected simPar bit3;

        /// <summary>
        /// Parametri simulazione oscillazione bit 4
        /// </summary>
        protected simPar bit4;

        /// <summary>
        /// Parametri simulazione oscillazione bit 5
        /// </summary>
        protected simPar bit5;

        /// <summary>
        /// Parametri simulazione oscillazione bit 6
        /// </summary>
        protected simPar bit6;

        /// <summary>
        /// Parametri simulazione oscillazione bit 7
        /// </summary>
        protected simPar bit7;

        /// <summary>
        /// pallet corrente
        /// </summary>
        protected int cP = 1;

        protected bool disableSimStatus = false;

        /// <summary>
        /// ultimo controllo decremento eventi
        /// </summary>
        protected DateTime lastEvCheck;

        /// <summary>
        /// Ultimo istante in cui sono stati ridotti dati simulazione duration
        /// </summary>
        protected DateTime lastRedDuration;

        /// <summary>
        /// Ultimo istante in cui sono stati generati dati di simulazione
        /// </summary>
        protected DateTime lastSimData;

        /// <summary>
        /// Cartella logfile x test import (tipo Soitaab)
        /// </summary>
        protected string logDirPath = "C:\\MesData\\TestSoitaab";

        /// <summary>
        /// Matricola OPR simulato
        /// </summary>
        protected int matrOpr = 1;

        /// <summary>
        /// Durata minima ODL x reset quando pezzi iob &gt; pezzi macchina...
        /// </summary>
        protected int minDurataODL = 480;

        /// <summary>
        /// pallet successivo (next)
        /// </summary>
        protected int nP = 1;

        /// <summary>
        /// periodo base del simulatore (in millisecondi)
        /// </summary>
        protected int periodoMSec = 1000;

        protected bool sendFluxOnRead = false;

        /// <summary>
        /// variabile di appoggio x stato segnale contapezzo
        /// </summary>
        protected bool sigPzCount = false;

        /// <summary>
        /// Simulazione registrazione dichiarazioni utente
        /// </summary>
        protected simPar simDich;

        /// <summary>
        /// BOOL: indica se simulare powerOn/Off (bit 0 e 1) compresi WarmUp e CoolDown
        /// </summary>
        protected bool simPowerOnOff = false;

        /// <summary>
        /// Simulazione effettuazione controlli utente
        /// </summary>
        protected simPar simRC;

        /// <summary>
        /// Simulazione registro scarti utente
        /// </summary>
        protected simPar simRS;

        /// <summary>
        /// Indica se va simulata come SLAVE
        /// </summary>
        protected bool simSlave = false;

        /// <summary>
        /// Tempo di MINIMO attesa x simulazione parametri
        /// </summary>
        protected int waitSimPar = utils.CRI("waitSimPar");

        #endregion Protected Fields

        #region Protected Methods

        /// <summary>
        /// Recupera file log da analizzare
        /// </summary>
        /// <returns></returns>
        protected override bool getRemoteLog()
        {
            bool answ = false;

            // leggo direttamente il file di test in area C:\MesData\TestSoitaab
            if (Directory.Exists(logDirPath))
            {
                string filePath = Path.Combine(logDirPath, $"Report-{DateTime.Today.Day}.txt");
                bool hasDaily = File.Exists(filePath);
                // cerco file quotidiano...
                if (!hasDaily)
                {
                    var fileList = Directory.GetFiles(logDirPath, "Report-*.txt");
                    DateTime lastMod = DateTime.Today.AddYears(-1);
                    foreach (var cFile in fileList)
                    {
                        var tempFile = Path.Combine(logDirPath, cFile);
                        var lwTime = File.GetLastWriteTime(tempFile);
                        if (lwTime >= lastMod)
                        {
                            lastMod = lwTime;
                            filePath = tempFile;
                        }
                    }
                }
                // leggo file
                string rawVal = File.ReadAllText(Path.Combine(logDirPath, filePath));
                if (!string.IsNullOrEmpty(rawVal))
                {
                    // salvo in redis
                    string redKey = redisMan.redHash($"IOB:CurrData:{IOBConfFull.General.CodIOB}:LogFile:Act");
                    redisMan.setRSV(redKey, rawVal);
                    answ = true;

                    // salvo SIMULAZIONE dati scambiati: lunghezza string
                    trackExchData(rawVal.Length, 1280);
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera da server set di dati specifici x IOB : qui per testare
        /// - versione std: compilazione files CSV (ex FTP Cimolai)
        /// - versione Kepware: test recupero parametri ricetta
        /// </summary>
        /// <returns></returns>
        protected override bool IobGetDataFromServer()
        {
            bool answ = false;
            if (!string.IsNullOrEmpty(getOptPar("SIM_KWP")))
            {
                answ = IobGetSendDossierKepware();
            }
            else
            {
                answ = IobGetDataFromServerToFtp();
            }
            return answ;
        }

        /// <summary>
        /// Salvo valori PLC
        /// </summary>
        /// <param name="updatedPar"></param>
        protected override void plcWriteParams(ref List<objItem> updatedPar)
        {
            dataConf currMem = null;
            int byteSize = 0;
            byte[] MemBlock = new byte[1];
            string memAddrWrite = "";
            string serObj = "";
            if (updatedPar != null)
            {
                // controllo i parametri... ne gestisco 4...
                foreach (var item in updatedPar)
                {
                    try
                    {
                        memAddrWrite = "";
                        // cerco in area memMapWrite...
                        if (memMap.mMapWrite.ContainsKey(item.uid))
                        {
                            // recupero!
                            currMem = memMap.mMapWrite[item.uid];
                            byteSize = currMem.size;
                            memAddrWrite = currMem.memAddr;
                            // salvo in memMap...
                            currMem.value = item.reqValue;
                            // registro x invio
                            item.value = item.reqValue;
                            item.reqValue = "";

                            MemBlock = new byte[byteSize];
                            serObj = JsonConvert.SerializeObject(item, Formatting.Indented);
                            lgInfo($"Inizio processing plcWriteParams per {currMem.name} | valore richiesto {currMem.value}{Environment.NewLine}---------------UPDATED PARAM---------------{Environment.NewLine}{serObj}{Environment.NewLine}---------------");
                            serObj = JsonConvert.SerializeObject(currMem, Formatting.Indented);
                            lgInfo($"---------------MEMORY CONTENT---------------{Environment.NewLine}{serObj}{Environment.NewLine}---------------");
                            lgInfo($"---------------MemBlock data---------------{Environment.NewLine}{BitConverter.ToString(MemBlock)}{Environment.NewLine}--------------- END data ---------------");

                            // salvo
                            upsertKey(item.uid, item.value);
                        }
                        else
                        {
                            lgInfo($"Errore uid non trovato in area write memory: {item.uid}, ci sono {memMap.mMapWrite.Count} in area write");
                        }
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in fase di plcWriteParams per item {item.uid} con valore {item.value}{Environment.NewLine}{exc}");
                    }
                }
            }
        }

        /// <summary>
        /// Effettua sync dati
        /// </summary>
        protected override void ProcessDataSync()
        {
            try
            {
                // "Ponte" sincrono -> asincrono
                Task.Run(async () =>
                {
                    // task non async
                    var taskWrite = iobWriteLocalCSV();
                    var taskSend = iobSendFTP("");
                    var taskGet = IobGetDataFromServer();

                    // Avviamo tutti i task Async contemporaneamente
                    var taskOdl = ProcessAutoOdlAsync();
                    var taskImport = ProcessFileImportAsync();
                    var taskRecipe = ProcessRecipeFileRetAsync();

                    // Attendiamo che TUTTI finiscano (Parallelismo)
                    await Task.WhenAll(taskOdl, taskImport, taskRecipe);

                    // ProcessAutoDossier (se è sincrono) lo chiamiamo qui o fuori
                    ProcessAutoDossier();
                })
                    .GetAwaiter()
                    .GetResult(); // Blocca qui finché tutto il gruppo è completo
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in ProcessDataSync: {exc.Message}");
            }
        }

        /// <summary>
        /// Altri task in slow frequency
        /// </summary>
        /// <returns></returns>
        protected override bool processSlowDataRead()
        {
            lgDebug($"Richiesto processSlowDataRead");
            bool answ = false;
            DateTime adesso = DateTime.Now;
            // faccio cmq tutto...

            string autoOdlRes = utils.callUrl(urlFixDailyOdl);

            // salvo lettura
            lastReadPLC = adesso;
            lgInfo($"Completato processSlowDataRead");
            return answ;
        }

        /// <summary>
        /// Decremento duration del bit indicato secondo stesse regole di ritardo dei bit di wait
        /// </summary>
        /// <param name="bit2proc"></param>
        protected void tryRedDuration(int bit2proc)
        {
            if (lastRedDuration.AddMilliseconds(periodoMSec / 20) < DateTime.Now)
            {
                switch (bit2proc)
                {
                    case 2:
                        bit2.duration--;
                        break;

                    case 3:
                        bit3.duration--;
                        break;

                    case 4:
                        bit4.duration--;
                        break;

                    case 5:
                        bit5.duration--;
                        break;

                    case 6:
                        bit6.duration--;
                        break;

                    case 7:
                        bit7.duration--;
                        break;

                    default:
                        break;
                }
                lastRedDuration = DateTime.Now;
            }
        }

        #endregion Protected Methods

        #region Private Fields

        private List<string> alarmMessages = new List<string>();

        private int currSimAlarmCode = 0;

        private Random rnd = new Random();

        #endregion Private Fields

        #region Private Methods

        /// <summary>
        /// Decodifica signal log x soitaab | FixMe ToDo: eliminare
        /// </summary>
        /// <param name="val2test"></param>
        /// <returns></returns>
        private static int decodeSoitaabLog(string val2test)
        {
            // di default NON EMERGENZA...
            int valInt = 128;
            switch (val2test)
            {
                case "START;1":
                    valInt = 1 + 2 + 128;
                    break;

                case "END;1":
                    valInt = 1 + 128;
                    break;

                case "START;0":
                case "END;0":
                case "HOLD;":
                    valInt = 1 + 16 + 128;
                    break;

                default:
                    break;
            }

            return valInt;
        }

        /// <summary>
        /// Decodifica il resto dell'area x i dati accessori (allarmi, ...)
        /// </summary>
        private void decodeOtherData()
        {
        }

        /// <summary>
        /// Effettua decodifica aree memoria alla bitmap usata x MAPO
        /// </summary>
        private void decodeToBaseBitmap()
        {
            // init a zero...
            B_input = 0;
            bool sendContapezzi = false;
            /* -----------------------------------------------------
             * bitmap MAPO
             * B0: POWER_ON
             * B1: RUN
             * B2: pzCount
             * B3: allarme
             * B4: manuale
             * B5: allarme TCiclo (SLOW)
             * B6: WarmUp_CoolDown
             * B7: EmergArmed (1 = NON emergenza, 0 = emergenza)
             * B8: pallet 1 (SE doppio pallet)
             * B9: pallet 2 (SE doppio pallet)
            ----------------------------------------------------- */

            // di base macchina in RUN
            B_input = 3;

            /*----------------------------------------
             * Simulazione segnali con priorità:
             * - Power ON / OFF (bit0/1)
             * - ALLARMI
             * - MANUALE
             * - SLOW
             * - contapezzi
             * - emergenza
             *
             *----------------------------------------*/

            // se simulo PowerOn/Off --> spegnimento con CoolDown e accensione con WarmUp..
            if (simPowerOnOff)
            {
                DateTime adesso = DateTime.Now;
                // se l'orario è dopo le tOff (tipicamente 22) --> NO RUN...
                if (adesso.Hour >= tOff || adesso.Hour <= tOn)
                {
                    // se prima/ultima mezz'ora è ancora accesa NON in run...
                    if (adesso.AddMinutes(-30).Hour < tOff || adesso.AddMinutes(30).Hour > tOn)
                    {
                        B_input = 1;
                    }
                    else
                    {
                        B_input = 0;
                    }
                    // aggiungo NON emergenza...
                    B_input += (1 << 7);
                }
            }
            // in primis verifico SE posso inviare in blocco i pezzi...... SE MP online e SE NON E' MULTI
            if (MPOnline)
            {
                // SE IOB online...
                if (IobOnline)
                {
                    // se il contapezzi è OLTRE il valore inviato....
                    if (contapezziPLC > contapezziIOB)
                    {
                        bool doSend = true;
                        // se è slave gestione verifica se deve davvero inviare o c'è stato un reset e aspetta...
                        if (simSlave)
                        {
                            // se idxOdl cambiato e contapezzi IOB resettato --> resetto...
                            int prevIdxOdl = currIdxODL;
                            pzCntReload(true);
                            bool changedOdl = prevIdxOdl != currIdxODL;
                            // anche se se variazione contapezzi oltre 1000--> resetto
                            bool countBigChange = contapezziPLC - contapezziIOB > 1000;
                            if (changedOdl || countBigChange)
                            {
                                contapezziPLC = contapezziIOB;
                                doSend = false;
                            }
                        }
                        if (doSend)
                        {
                            string machName = "";
                            if (isMulti)
                            {
                                machName = $"{IOBConfFull.General.CodIOB}|TAV_{nP}";
                            }
                            // invio SOLO SE sono OLTRE i numSim pz e li invio TUTTI in blocco
                            if ((contapezziPLC - contapezziIOB) > minSendPzCountBlock)
                            {
                                sigPzCount = true;
                                trySendPzCountBlock(machName, contapezziIOB, contapezziPLC);
                                sigPzCount = false;
                            }
                            // altrimenti invio 1 segnale
                            else
                            {
                                // se NON STAVA inviando di già...
                                if (!sigPzCount)
                                {
                                    // segnalo BIT (1 pz)
                                    B_input += (1 << 2);
                                    sigPzCount = true;
                                    // salvo nuovo contapezzi (incremento di 1...) + richiesta refresh conteggio
                                    contapezziIOB++;
                                    needRefreshPzCount = true;
                                    // invio conferma contapezzi..
                                    string fullUrl = $"{urlSetPzCount}{contapezziIOB}";
                                    if (isMulti)
                                    {
                                        fullUrl = $"{urlSetPzCount.Replace(IOBConfFull.General.CodIOB, $"{IOBConfFull.General.CodIOB}|TAV_{nP}")}{contapezziPLC}";
                                    }
                                    string retVal = utils.callUrl(fullUrl);
                                    // verifica salvataggio
                                    if (retVal != contapezziIOB.ToString())
                                    {
                                        // errore salvataggio contapezzi
                                        lgInfo($"Errore salvataggio Contapezzi SIMULA 01: contapezziIOB {contapezziIOB} | risposta: {retVal}");
                                        // rileggo il counter pezzi da server
                                        pzCntReload(true, machName);
                                    }
                                }
                                else
                                {
                                    string retVal = utils.callUrl($"{urlSetPzCount}{contapezziIOB}");
                                    if (retVal != contapezziIOB.ToString())
                                    {
                                        // errore salvataggio contapezzi
                                        lgInfo($"Errore salvataggio Contapezzi SIMULA 02: contapezziIOB {contapezziIOB} | risposta: {retVal}");
                                        if (!isMulti)
                                        {
                                            // rileggo il counter pezzi da server
                                            pzCntReload(true, machName);
                                        }
                                    }
                                    sigPzCount = false;
                                }
                            }
                            lgInfo($"S01: Valori contatori: contapezzi macchina contapezziPLC: {contapezziPLC} | contapezziIOB: {contapezziIOB} | contapezziPLC > contapezziIOB");
                            // salvo SIMULAZIONE dati scambiati: contapezzi 8 byte (parziale, totale, richiesti, mancanti)
                            trackExchData(16, 768);
                        }
                    }
                }
                else
                {
                    // imposto currODL a vuoto!
                    currODL = "";
                    if (periodicLog)
                    {
                        lgInfo($"SIMULA | Lettura ODL non effettuata: IobOnline: {IobOnline} | currODL impostato a vuoto");
                    }
                }
            }

            // questa parte la processo SOLO SE sono in run --> B_input == 3
            if (B_input == 3 || B_input == 7)
            {
                // di base NON emergenza
                B_input += (1 << 7);
                // ora controllo il resto...
                if (bit3 != null && bit3.wait <= 0)
                {
                    // segnalo BIT
                    B_input += (1 << 3);
                    // se non ancora fatto, simulo insorgenza allarme
                    if (currSimAlarmCode == 0)
                    {
                        // simulo una bitmap di max 8 allarmi, 1..255
                        currSimAlarmCode = rnd.Next(1, 255);
                        // invio allarme, 0 --> sim
                        sendAlarmVariations("SIM_BANK", 0, 0, (uint)currSimAlarmCode, alarmMessages);
                    }
                    // decremento duration
                    tryRedDuration(3);
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (bit3.duration <= 0)
                    {
                        bit3 = setupSimPar("SIM_ALARM");
                        // invio fine allarme: sim --> 0
                        sendAlarmVariations("SIM_BANK", 0, (uint)currSimAlarmCode, 0, alarmMessages);
                        // simulo terminazione allarme
                        currSimAlarmCode = 0;
                    }

                    tryConfirm();
                }
                else if (bit6 != null && bit6.wait <= 0)
                {
                    // segnalo BIT
                    B_input += (1 << 6);
                    // decremento duration
                    tryRedDuration(6);
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (bit6.duration <= 0)
                    {
                        bit6 = setupSimPar("SIM_WUCD");
                    }
                    // in warm up: provo split ODL
                    ProcessAutoOdl();
                }
                else if (bit7 != null && bit7.wait <= 0)
                {
                    // segnalo BIT NEGATO (emergenza)
                    B_input -= (1 << 7);
                    // decremento duration
                    tryRedDuration(7);
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (bit7.duration <= 0)
                    {
                        bit7 = setupSimPar("SIM_EMRG");
                    }
                    // in emergenza: provo split ODL
                    ProcessAutoOdl();
                }
                else if (bit4 != null && bit4.wait <= 0)
                {
                    // segnalo BIT
                    B_input += (1 << 4);
                    // decremento duration
                    tryRedDuration(4);
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (bit4.duration <= 0)
                    {
                        bit4 = setupSimPar("SIM_MANU");
                    }
                    // in manuale: provo split ODL
                    ProcessAutoOdl();
                }
                else if (bit2 != null && bit2.wait <= 0)
                {
                    // salvo nuovo contapezziPLC
                    var rand = new Random();
                    // se online vero delta (1..10) altrimenti 1
                    int delta = IobOnline ? rand.Next(1, 10) : 1;
                    if (MPOnline)
                    {
                        // solo se MP online...
                        contapezziPLC += delta;
                        lgInfo($"S01: Valori contatori: contapezzi macchina contapezziPLC: {contapezziPLC} | contapezziIOB: {contapezziIOB} | aggiunto delta {delta}");
                    }
                    // SOLO SE sono online...
                    if (IobOnline)
                    {
                        // se multipallet --> cP a zero!
                        if (isMulti)
                        {
                            cP = 0;
                        }
                        // se NON Multi fa contapezzi...
                        else
                        {
                            // SE NON SONO GIA' OLTRE il contapezzi
                            if (contapezziIOB < contapezziPLC)
                            {
                                // segnalo BIT (1 pz)
                                B_input += (1 << 2);
                            }
                        }
                        // decremento duration
                        tryRedDuration(2);
                        // controllo se sia scaduta la duration... in quel caso reset...
                        if (bit2.duration <= 0)
                        {
                            bit2 = setupSimPar("SIM_PZCNT");
                            // salvo nuovo contapezzi (incremento di 1...) + richiesta refresh conteggio
                            contapezziIOB++;
                            needRefreshPzCount = true;
                            lgInfo($"S01: Valori contatori: contapezzi macchina contapezziPLC: {contapezziPLC} | contapezziIOB: {contapezziIOB} - incremento contapezzi per bit2.duration <= 0");
                            // invio SOLO SE il contapezzi PLC è != contapezzi IOB...
                            if (contapezziPLC != contapezziIOB)
                            {
                                sendContapezzi = true;
                            }
                        }
                        if (sendContapezzi)
                        {
                            // controllo se ALMENO sia pingabile il server
                            bool srvAlive = CheckServerAlive();
                            if (srvAlive)
                            {
                                string fullUrl = $"{urlSetPzCount}{contapezziIOB}";
                                // invio a server contapezzi (aggiornato)
                                if (isMulti)
                                {
                                    fullUrl = $"{urlSetPzCount.Replace(IOBConfFull.General.CodIOB, $"{IOBConfFull.General.CodIOB}|TAV_{nP}")}{contapezziIOB}";
                                }
                                string retVal = utils.callUrl(fullUrl);
                                // verifica se tutto OK
                                if (retVal != contapezziIOB.ToString())
                                {
                                    // errore salvataggio contapezzi
                                    lgInfo($"Errore salvataggio Contapezzi SIMULA 03: contapezziIOB {contapezziIOB} | risposta: {retVal}");
                                    // se non sono multi...
                                    if (!isMulti)
                                    {
                                        // rileggo il counter pezzi da server
                                        pzCntReload(true);
                                    }
                                }
                                // resetto timer...
                                lastPzCountSend = DateTime.Now;
                            }
                        }
                        // provo a fare split ODL...
                        ProcessAutoOdl();
                    }
                    else
                    {
                        // decremento duration
                        tryRedDuration(2);
                        if (bit2.duration <= 0)
                        {
                            bit2 = setupSimPar("SIM_PZCNT");
                        }
                    }
                }
                else if (bit5 != null && bit5.wait <= 0 || tcMan.alarmDelayTC)
                {
                    // segnalo BIT
                    B_input += (1 << 5);
                    // decremento duration
                    tryRedDuration(5);
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (bit5.duration <= 0)
                    {
                        bit5 = setupSimPar("SIM_SLOW");
                    }
                }
                // se multi gestisco il bit delle tavole...
                if (isMulti)
                {
                    // se sono in fase di fronte d'uscita (invio contapezzi) INVERTO nP...
                    if (sendContapezzi)
                    {
                        nP = nP == 1 ? 2 : 1;
                        // assegno a cP il valore nP...
                        cP = nP;
                        contapezziPLC++;
                        // invio contapezzi...
                        string fullUrl = $"{urlSetPzCount.Replace(IOBConfFull.General.CodIOB, $"{IOBConfFull.General.CodIOB}|TAV_{nP}")}{contapezziIOB}";
                        string retVal = utils.callUrl(fullUrl);
                    }
                    // se cP > 0 --> segnalo bit tavola...
                    if (cP == 1)
                    {
                        B_input += (1 << 8);
                    }
                    else if (cP == 2)
                    {
                        B_input += (1 << 9);
                    }
                }

                // gestione simulazione utente
                if (simRC != null && simRC.wait <= 0)
                {
                    // preparo record controlli... registro controllo se mi esce un secondo divisibile x 25
                    DateTime adesso = DateTime.Now;
                    int esitoNum = adesso.Second % 25 == 0 ? 0 : 1;
                    string note = esitoNum == 1 ? "" : $"Controllo pezzo KO alle {DateTime.Now:yyyy-MM-dd HH:mm:ss} (simulato)";
                    string sVal = $"MatrOpr: {matrOpr} | Esito: {esitoNum} | note: {note}";
                    // accodo x invio
                    accodaUserLog(sVal, qEncodeULog("RC", note, matrOpr, "", esitoNum));
                    // decremento duration
                    simRC.duration--;
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (simRC.duration <= 0)
                    {
                        simRC = setupSimPar("SIM_RC");
                    }
                }
                if (simRS != null && simRS.wait <= 0)
                {
                    // preparo record controlli... guasto se mi esce un secondo divisibile x 25
                    DateTime adesso = DateTime.Now;
                    int causaleInt = adesso.Second % 7 + 1;
                    int numSca = adesso.Second % 5 + 1;
                    string causale = $"{causaleInt:00}";
                    string note = $"SIM Scarto [{causale} x {numSca}] {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
                    string sVal = $"MatrOpr: {matrOpr} | Causale: {causale} | note: {note}";
                    // accodo x invio
                    accodaUserLog(sVal, qEncodeULog("RS", note, matrOpr, causale, numSca));
                    // decremento duration
                    simRS.duration--;
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (simRS.duration <= 0)
                    {
                        simRS = setupSimPar("SIM_RS");
                    }
                }
                if (simDich != null && simDich.wait <= 0)
                {
                    // preparo record dichiarazione...
                    DateTime adesso = DateTime.Now;
                    bool recLogin = adesso.Second % 5 == 0 ? false : true;
                    string note = recLogin ? "SIM Login Utente STEAMWARE USER" : $"SIM Nota automatica alle {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
                    string codTag = recLogin ? "UserLogin" : "Nota";
                    string sVal = $"MatrOpr: {matrOpr} | codTag: {codTag} | nota: {note}";
                    // accodo x invio
                    accodaUserLog(sVal, qEncodeULog("DI", note, matrOpr, codTag, 0));
                    // decremento duration
                    simDich.duration--;
                    // controllo se sia scaduta la duration... in quel caso reset...
                    if (simDich.duration <= 0)
                    {
                        simDich = setupSimPar("SIM_DICH");
                    }
                }

                // init obj display
                newDisplayData currDispData = new newDisplayData();
                currDispData.counter = contapezziIOB;
                currDispData.semOut = Semaforo.SV;
                raiseRefresh(currDispData);
            }

            // salvo SIMULAZIONE dati scambiati: 64byte banchi memoria
            trackExchData(128, 2048);
        }

        /// <summary>
        /// Recupero dati da server x costruzione files CSV da inviare via FTP (ex: Cimolai)
        /// </summary>
        /// <returns></returns>
        private bool IobGetDataFromServerToFtp()
        {
            bool answ;
            bool okArt = false;
            bool okDoss = false;
            bool okPodl = false;
            bool okLVFasi = false;
            List<PODLModel> listaPODL = new List<PODLModel>();
            List<DossiersModel> listaDoss = new List<DossiersModel>();
            List<AnagArticoli> listaArt = new List<AnagArticoli>();
            List<ListVal> anagLVFasi = new List<ListVal>();
            Dictionary<string, string> dictAF = new Dictionary<string, string>();
            // recupera dati da server tramite chiamate REST a MP/IO/IOB...

            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    var rawListArt = await callUrl(urlGetCurrArt, false);
                    var rawListDOSS = await callUrl(urlGetCurrDOSS, false);
                    var rawListPODL = await callUrl(urlGetNextPODL, false);
                    var rawLVFasi = await callUrl(urlGetListValFasiPodl, false);

                    if (!string.IsNullOrEmpty(rawListArt))
                    {
                        try
                        {
                            listaArt = JsonConvert.DeserializeObject<List<AnagArticoli>>(rawListArt);
                            okArt = listaArt.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco ART ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco ART ({urlGetCurrArt}) ha restituito valore vuoto");
                    }
                    if (!string.IsNullOrEmpty(rawListDOSS))
                    {
                        try
                        {
                            listaDoss = JsonConvert.DeserializeObject<List<DossiersModel>>(rawListDOSS);
                            okDoss = listaDoss.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco DOSSIER ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco DOSSIER ({urlGetCurrDOSS}) ha restituito valore vuoto");
                    }
                    if (!string.IsNullOrEmpty(rawListPODL))
                    {
                        try
                        {
                            listaPODL = JsonConvert.DeserializeObject<List<PODLModel>>(rawListPODL);
                            okPodl = listaPODL.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco DOSSIER ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco PODL ({urlGetNextPODL}) ha restituito valore vuoto");
                    }
                    if (!string.IsNullOrEmpty(rawLVFasi))
                    {
                        try
                        {
                            anagLVFasi = JsonConvert.DeserializeObject<List<ListVal>>(rawLVFasi);
                            dictAF = anagLVFasi.ToDictionary(x => x.value, x => x.label);
                            okLVFasi = listaArt.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco ListVal ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco ListVal ({urlGetListValFasiPodl}) ha restituito valore vuoto");
                    }
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in IobGetDataFromServerToFtp | {Environment.NewLine}{ex.Message}");
            }
            // continuo
            answ = okPodl && okDoss && okArt && okLVFasi;
            if (answ)
            {
                // predispongo dati PODL
                ListaJobs = listaPODL
                                .Select(x => new JobRow() { Matricola = x.CodArticolo, Commessa = $"PODL{x.IdxPromessa:00000000}", Articolo = x.CodArticolo, Descrizione = x.CodArticolo, DataIns = $"{x.InsertDate:dd/MM/yyyy}", OraIns = $"{x.InsertDate:HH:mm}", Lavorazione = $"{getLV(dictAF, x.KeyRichiesta)} {x.Note}".Trim() })
                                .ToList();

                // predispongo dati articoli
                ListaArticoli = listaArt
                                .Select(a => new ArtRow() { Matricola = a.CodArticolo, Articolo = a.Disegno, Descrizione = a.DescArticolo, LimiteVel = 100 })
                                .Distinct()
                                .ToList();

                // completo con dati DOSSIER
                foreach (var item in ListaArticoli)
                {
                    string codArt = item.Matricola;
                    var currDoss = listaDoss.Where(x => x.CodArticolo == codArt).FirstOrDefault();
                    if (currDoss != null)
                    {
                        DossierFluxLogDTO resultSet = JsonConvert.DeserializeObject<DossierFluxLogDTO>(currDoss.Valore);
                        // traduco AD MENTULAM...
                        item.Peso_01 = getFluxValInt(resultSet, "POS_X");
                        item.Peso_02 = getFluxValInt(resultSet, "POS_Y");
                        item.Peso_03 = getFluxValInt(resultSet, "RAPID_OVER");
                        item.Peso_04 = getFluxValInt(resultSet, "POS_Z");
                        item.PosizCarrello_01 = getFluxValInt(resultSet, "TEMP_01");
                        item.PosizCarrello_02 = getFluxValInt(resultSet, "RAPID_OVER");
                        item.PosizCarrello_03 = getFluxValInt(resultSet, "POWER_01");
                        item.PosizCarrello_04 = getFluxValInt(resultSet, "POS_X");
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupero dati da server x invio dati ricetta a KepWare (Ex RAMA x TFT)
        /// </summary>
        /// <returns></returns>
        private bool IobGetSendDossierKepware()
        {
            bool answ;

            lgInfo("iobGetSendDossier: Inizio scrittura dati a KepWare");
            /*-----------------------------------------
             * STEPS processo:
             * - recupero ARTICOLI (solo quelli dei PODL attivi)
             * - recupero PODL (tutti)
             * - recupero elenco DOSSIER last x macchina (legato ai PODL attivi)
             * - prendo SOLO PODL attivi, e prendo il + vecchio
             * - se trovo dossier x articolo del PODL + vecchio (e dovrebbe...) procedo
             * - uso Dictionary RecipeKeyTranslate x tradurre l'elenco fluxLog in nuovi valori
             * - preparo il set di valori da scrivere con traduzione
             * - invio scritture (sim test --> scrivo su file?)
             * - aggiungo info articolo (+ ODL...)
             * - aggiungo il valore di "scrittura effettuata"
             *----------------------------------------- */

            bool okArt = false;
            bool okDoss = false;
            bool okPodl = false;
            List<AnagArticoli> listaArt = new List<AnagArticoli>();
            List<PODLModel> listaPODL = new List<PODLModel>();
            List<DossiersModel> listaDoss = new List<DossiersModel>();
            // recupera dati da server tramite chiamate REST a MP/IO/IOB...
            try
            {
                // PONTE SYNC/ASYNC: Ora sw.Stop() aspetterà la fine reale dell'operazione
                Task.Run(async () =>
                {
                    var rawListArt = await callUrl(urlGetCurrArt, false);
                    var rawListDOSS = await callUrl(urlGetCurrDOSS, false);
                    var rawListPODL = await callUrl(urlGetNextPODL, false);
                    if (!string.IsNullOrEmpty(rawListArt))
                    {
                        try
                        {
                            listaArt = JsonConvert.DeserializeObject<List<AnagArticoli>>(rawListArt);
                            okArt = listaArt.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco ART ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco ART ({urlGetCurrArt}) ha restituito valore vuoto");
                    }
                    if (!string.IsNullOrEmpty(rawListDOSS))
                    {
                        try
                        {
                            listaDoss = JsonConvert.DeserializeObject<List<DossiersModel>>(rawListDOSS);
                            okDoss = listaDoss.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco DOSSIER ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco DOSSIER ({urlGetCurrDOSS}) ha restituito valore vuoto");
                    }
                    if (!string.IsNullOrEmpty(rawListPODL))
                    {
                        try
                        {
                            listaPODL = JsonConvert.DeserializeObject<List<PODLModel>>(rawListPODL);
                            okPodl = listaPODL.Count > 0;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore: chiamata elenco DOSSIER ha restituito errore{Environment.NewLine}{exc}");
                        }
                    }
                    else
                    {
                        lgError($"Errore: chiamata elenco PODL ({urlGetNextPODL}) ha restituito valore vuoto");
                    }
                })
                .GetAwaiter()
                .GetResult();
            }
            catch (Exception ex)
            {
                lgError($"Errore in IobGetSendDossierKepware | {Environment.NewLine}{ex.Message}");
            }
            // continuo
            answ = okArt && okPodl && okDoss;
            if (answ)
            {
                // verifico i PODL attivi...
                var listPodlAct = listaPODL.Where(x => x.Attivabile).OrderBy(x => x.InsertDate).ToList();
                if (listPodlAct.Count > 0)
                {
                    string codArt = listPodlAct.FirstOrDefault().CodArticolo;
                    // verifico di avere il dossier e l'articolo x questo PODL
                    var selArt = listaArt.Where(x => x.CodArticolo == codArt).FirstOrDefault();
                    var selDoss = listaDoss.Where(x => x.CodArticolo == codArt).FirstOrDefault();
                    // prendo il primo e recupero...
                    if (selArt != null && selDoss != null)
                    {
                        // recupero il resultset dei valori FluxLog da caricare
                        DossierFluxLogDTO resultSet = JsonConvert.DeserializeObject<DossierFluxLogDTO>(selDoss.Valore);
                        List<FluxLog> listFluxLog = resultSet.ODL;

                        // preparo elenco parametri da inviare...
                        list2Write = new List<objItem>();
                        // per ogni valore ricevuto nel dossier --> calcolo parametro da inviare
                        foreach (var item in listFluxLog)
                        {
                            objItem currData = new objItem()
                            {
                                // valori dentro OPC-UA
                                //uid = $"{opcUaParams.BaseKeyTranslate}{item.CodFlux}",
                                uid = item.CodFlux,
                                reqValue = item.Valore,
                                name = item.CodFlux
                            };

                            // aggiungo!
                            list2Write.Add(currData);
                        }
                        // effettua chiamata scrittura verso impianto...
                        plcWriteParams(ref list2Write);
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Esegue processing + invio dati tab SignLog
        /// </summary>
        /// <returns></returns>
        private bool ProcessFluxLogTable()
        {
            bool fatto = false;
            // leggo eventuali dati di fluxlog ed invio...
            if (fluxLogData != null && fluxLogData.Count > 0)
            {
                string sVal = "";
                foreach (var fLog2send in fluxLogData)
                {
                    var kvpFlux = fLog2send.valString.Split(';');
                    if (kvpFlux.Count() > 1)
                    {
                        // verifico NON sia un ND...
                        if (kvpFlux[0] == "ND")
                        {
                            // log anomalia...
                            lgTrace($"Errore in ProcessFluxLogTable: kvpFlux[0] risulta ND! | item.key: {kvpFlux[0]} | item.Value: {kvpFlux[1]}");
                        }
                        else
                        {
                            sVal = $"[DYNDATA] |{fLog2send.dtRif:yyyy-MM-dd HH:mm:ss}|{kvpFlux[0]}|{kvpFlux[1]}";
                            // chiamo accodamento con dataora corretta...
                            bool sent = accodaFLog(kvpFlux[0], sVal, qEncodeFLog(fLog2send.dtRif, kvpFlux[0], kvpFlux[1]));
                            if (sent)
                            {
                                // traccio valore DynData x analisi
                                trackDynData(kvpFlux[0], kvpFlux[1]);

                                // salvo SIMULAZIONE dati scambiati: lunghezza val string
                                trackExchData(fLog2send.valString.Length, 1536);
                            }
                        }
                    }
                }
                // svuoto coda invio fluxlog...
                fluxLogData = new List<GenLogRow>();
                // fatto!
                fatto = true;
            }
            return fatto;
        }

        /// <summary>
        /// Esegue processing + invio dati tab SignLog
        /// </summary>
        /// <param name="adesso"></param>
        /// <returns></returns>
        private bool ProcessSignLogTable(DateTime adesso)
        {
            bool fatto = false;
            // init oggetto x processing
            Dictionary<DateTime, int> sigLogFromFile = new Dictionary<DateTime, int>();
            // recupero i dati dai logs files, attuale e nuovo...
            string fileAct = redisMan.getRSV(redKeyLogfileAct);
            string fileLast = redisMan.getRSV(redKeyLogfileLast);
            // confronto con i dati dell'ultimo LOG FILE processato
            int lenghtAct = fileAct != null ? fileAct.Length : 0;
            int lenghtLast = fileLast != null ? fileLast.Length : 0;
            //if (lenghtAct > 0 && lenghtAct != lenghtLast)
            if (lenghtAct > 0)
            {
                List<GenLogRow> logNew = new List<GenLogRow>();
                List<GenLogRow> sigLogData = new List<GenLogRow>();
                List<GenLogRow> logLast = getGenLogFromMachineLog(fileLast);
                List<GenLogRow> logAct = getGenLogFromMachineLog(fileAct);
                // SE c'è prendo ultima riga inviata x confronto...
                var fileLastEnd = logLast.LastOrDefault();
                if (fileLastEnd != null)
                {
                    // ora prendo SOLAMENTE le righe nuove
                    logNew = logAct
                                    .Where(x => x.dtRif > fileLastEnd.dtRif)
                                    .ToList();
                }
                else
                {
                    logNew = logAct;
                }
                // solo SE ho nuovi dati....
                if (logNew.Count > 0)
                {
                    // processo le righe nuove e le accodo in redis come elenco FluxLog da inviare
                    // (appoggio in redis)... andando ad accodarle...
                    var currFLD = fluxLogData;
                    currFLD.AddRange(logNew);
                    fluxLogData = currFLD;

                    // estraggo solo info x sig log
                    sigLogData = logNew
                                .Where(x => x.valString.StartsWith("START") || x.valString.StartsWith("HOLD") || x.valString.StartsWith("END"))
                                .OrderBy(x => x.dtRif)
                                .ToList();

                    // processo le righe nuove trasformando al volo SOLO eventi...
                    foreach (var sLog2send in sigLogData)
                    {
                        /* -----------------------------------------------------
                        *   bitmap MAPO STANDARD 60
                        *   B0: 001 POWER_ON
                        *   B1: 002 RUN
                        *   B2: 004 pzCount
                        *   B3: 008 allarme
                        *   B4: 016 manuale
                        *   B5: 032 slowTC
                        *   B6: 064 WarmUpCoolDown
                        *   B7: 128 EmergArmata
                        *
                        ----------------------------------------------------- */

                        int valInt = decodeSoitaabLog(sLog2send.valString);

                        string currVal = getEncodSigLog(sLog2send.dtRif, valInt, counterSigIN);
                        // verifico non sia in veto invio iniziale...
                        if (queueInEnabCurr)
                        {
                            // --> accodo (valore già formattato)!
                            QueueIN.Enqueue(currVal);
                            // loggo!
                            lgDebug(string.Format("[QUEUE-IN] {0}", currVal));
                            counterSigIN++;
                            if (counterSigIN > 9999)
                            {
                                counterSigIN = 0;
                            }
                        }
                        else
                        {
                            lgDebug($"[VETO FOR QUEUE-IN] | {currVal} - MESSAGE NOT SENT | {adesso:yyyyMMdd_HHmmss}");
                            checkVetoQueueIn();
                        }
                    }

                    /* ----------------------------------------------------------
                     * processo righe x costruire una lista di eventi produzione:
                     * - elenco di eventi contapezzi
                     * - lista xODL come articolo + quantità prodotta (_1, _2, ...)
                     * - eventuale ultimo xODL che resta aperto
                     *
                     * successivamente processing + invio info raccolte
                     * - recupero PODL aperti
                     * - apertura/chiusura PODL esistenti
                     * - creazione PODL mancanti
                     * - dichiarazione contapezzi
                     *
                     * ---------------------------------------------------------- */

                    string sVal = "";
                    string descrArt = "";
                    List<ProdBatchData> elencoProdBatch = new List<ProdBatchData>();
                    ProdBatchData lastProdBatch = new ProdBatchData();

                    // ora processo TUTTO x il generico caso fluxLog......
                    foreach (var fLog2send in logNew)
                    {
                        // separo per ";"
                        var currData = fLog2send.valString.Split(';');
                        if (currData.Length > 1)
                        {
                            if (sendFluxOnRead)
                            {
                                // verifico NON sia un ND...
                                if (currData[0] == "ND")
                                {
                                    // log anomalia...
                                    lgTrace($"Errore in ProcessSignLogTable: kvpFlux[0] risulta ND! | item.key: {currData[0]} | item.Value: {currData[1]}");
                                }
                                else
                                {
                                    // per prima cosa fluxLog a prescindere...
                                    sVal = $"[LOGFILE]{currData[0]}|{currData[1]}";
                                    // ...e chiamo accodamento
                                    bool sent = accodaFLog(currData[0], sVal, qEncodeFLog(fLog2send.dtRif, currData[0], currData[1]));
                                    if (sent)
                                    {
                                        // traccio valore DynData x analisi
                                        trackDynData(currData[0], currData[1]);

                                        // salvo SIMULAZIONE dati scambiati: lunghezza val string
                                        trackExchData(fLog2send.valString.Length, 1024);
                                    }
                                }
                            }

                            switch (currData[0])
                            {
                                case "END":
                                    // se è un end --> è comunque NON + running
                                    isRunningState = false; // chiudo batch precedente + in elenco (SE era valido...)
                                    if (!string.IsNullOrEmpty(lastProdBatch.codArt))
                                    {
                                        lastProdBatch.dtEnd = fLog2send.dtRif;
                                        elencoProdBatch.Add(lastProdBatch);
                                    }
                                    // reset articolo..
                                    lastArtDescr = "";
                                    // nuovo batch VUOTO
                                    lastProdBatch = new ProdBatchData();
                                    contapezziPLC = 0;
                                    break;

                                case "START":
                                    isRunningState = currData[1] == "1";
                                    break;

                                case "PARTCODE":
                                    // inizio confronto articolo con precedente x capire se sia NUOVO
                                    descrArt = currData[1];
                                    // devo scartare i "doppi percorsi"
                                    if (descrArt != lastArtDescr)
                                    {
                                        // verifico se sia DAVVERO cambiato articolo... prendo
                                        // articolo senza indice (_1, _2, _3, ...)
                                        var artDataLast = lastArtDescr.Split('_');
                                        var artDataNew = descrArt.Split('_');
                                        // nuovo articolo (NON vuoto)
                                        if (!string.IsNullOrEmpty(artDataNew[0]) && artDataLast[0] != artDataNew[0])
                                        {
                                            // chiudo batch precedente + in elenco (SE era valido...)
                                            if (!string.IsNullOrEmpty(lastProdBatch.codArt))
                                            {
                                                // chiudo prod
                                                lastProdBatch.dtEnd = fLog2send.dtRif;
                                                elencoProdBatch.Add(lastProdBatch);
                                            }
                                            // nuovo batch
                                            lastProdBatch = new ProdBatchData()
                                            {
                                                codArt = artDataNew[0],
                                                dtStart = fLog2send.dtRif,
                                                numPz = 1
                                            };
                                        }
                                        // solo nuova copia articolo
                                        else
                                        {
                                            // aumento numPezzi batch
                                            lastProdBatch.numPz++;
                                        }

                                        // salvo nuovo art processato
                                        lastArtDescr = descrArt;
                                    }
                                    break;

                                default:
                                    break;
                            }
                        }
                    }

                    // recupero elenco PODL corrente...
                    List<PODLModel> reqPOdlList = MachineNextPodl();
                    // adesso processo tutti i prodBatch collezionati x invio
                    foreach (var item in elencoProdBatch)
                    {
                        int currIdxPOdl = 0;
                        // cerco se articolo sia in elenco PODL
                        var listPOdl = reqPOdlList
                                        .Where(x => x.CodArticolo.Equals(item.codArt, StringComparison.InvariantCultureIgnoreCase))
                                        .ToList();

                        // se trovato --> invio start/stop PODL
                        if (listPOdl != null && listPOdl.Count > 0)
                        {
                            var currPodl = listPOdl.OrderBy(x => x.IdxPromessa).FirstOrDefault();
                            if (currPodl != null)
                            {
                                currIdxPOdl = currPodl.IdxPromessa;
                                // elimino da elenco PODL
                                reqPOdlList.Remove(currPodl);
                            }
                        }
                        else
                        {
                            // se NON trovato --> creo PODL, poi avvio/chiudo
                            currIdxPOdl = TryCreatePodl(item.codArt, CodGruppoIob, item.numPz);
                        }
                        // se idxPOdl valido
                        if (currIdxPOdl > 0)
                        {
                            // mando apertura PODL
                            SendStartPodl(currIdxPOdl, item.dtStart);
                            // chiudo PODL...
                            if (item.dtEnd != null)
                            {
                                // invio pezzi 3 sec prima di chiusura
                                SendPzIncrAtDate(item.numPz, ((DateTime)item.dtEnd).AddSeconds(-10));
                                // chiudo
                                SendClosePOdl(currIdxPOdl, (DateTime)item.dtEnd);
                            }
                        }
                    }

                    // salvo in last il valore di act...
                    redisMan.setRSV(redKeyLogfileLast, fileAct);
                }
                fatto = true;
            }
            return fatto;
        }

        /// <summary>
        /// Effettua invio a MP/IO dell'elenco serializzato dei dataItems
        /// </summary>
        /// <param name="dataItems"></param>
        private void sendDataItemsList(List<machDataItem> dataItems)
        {
            string rawData = JsonConvert.SerializeObject(dataItems);
            utils.CallUrlPost($"{urlSaveDataItems}", rawData);
        }

        /// <summary>
        /// Setup parametri di simulazione per BIT indicato
        /// </summary>
        /// <param name="keyName"></param>
        private simPar setupSimPar(string keyName)
        {
            simPar answ = new simPar();
            if (IOBConfFull.OptPar.Count > 0)
            {
                if (IOBConfFull.OptPar.ContainsKey(keyName))
                {
                    string fullVal = getOptPar(keyName);
                    if (!string.IsNullOrEmpty(fullVal) && fullVal.IndexOf("|") > 0)
                    {
                        string[] param = fullVal.Split('|');
                        int.TryParse(param[0], out answ.wait);
                        int.TryParse(param[1], out answ.duration);
                        // simulo valore wait: in media target, range +/- 80%
                        answ.wait = rnd.Next(answ.wait / 5, answ.wait * 9 / 5);
                        // simulo valore duration: in media target, range +/- 50%
                        answ.duration = rnd.Next(answ.duration / 2, answ.duration * 3 / 2);
                        // verifico siano > 0...
                        answ.wait = answ.wait > 1 ? answ.wait : 2;
                        answ.duration = answ.duration > 1 ? answ.duration : 2;
                    }
                }
            }
            return answ;
        }

        /// <summary>
        /// Provo ad effettuare conferma pezzi SE sono oltre uno scalino del 10% dall'ultima conferma...
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        private void tryConfirm()
        {
        }

        #endregion Private Methods
    }
}