using IOB_UT_NEXT;
using IOB_UT_NEXT.Config;
using IOB_UT_NEXT.DataModel;
using MapoSDK;
using Newtonsoft.Json;
using NLog;
using Opc.Ua;
using Opc.Ua.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static IOB_UT_NEXT.ByteDataConverter;

namespace IOB_WIN_OPC_UA.IobOpc
{
    /// <summary>
    /// Classe adapter OPC-UA di base
    /// </summary>
    public class OpcUa : Iob.GenericNext
    {
        #region Public Constructors

        /// <summary>
        /// Estende l'init della classe base, impiegando il pacchetto Nuget OPC-UA foundation https://github.com/OPCFoundation/UA-.NETStandard
        /// </summary>
        /// <param name="caller">Form chiamante</param>
        /// <param name="IobConfFull">Configurazione (v 4.x)</param>
        public OpcUa(AdapterFormNext caller, IobConfTree IobConfFull) : base(caller, IobConfFull)
        {
            lgInfo("Init OpcUa...");
            // inizializzo...
            subscribedItems = new List<Opc.Ua.Client.MonitoredItem>();
            VetoSlowDataRead = DateTime.Now.AddSeconds(IOBConfFull.FluxLog.SamplePeriod);
            enableDataFilter = IOBConfFull.FluxLog.EnableFilt;
            enableCliRestart = IOBConfFull.Device.Connect.EnableRestart;
            enableSlowData = IOBConfFull.FluxLog.EnableSlowData;
            maxTryPing = IOBConfFull.Device.Connect.MaxTryPing;
            samplePeriodBase = IOBConfFull.FluxLog.SamplePeriod;
            MaxSecReload = IOBConfFull.FluxLog.MaxSecReload;
            readErrorMax = IOBConfFull.Device.ReadErrorMax;
            readErrorSleepTime = IOBConfFull.Device.ReadErrorSleepTime;

            // init datetime counters
            DateTime adesso = DateTime.Now;
            lastPzCountSend = adesso;
            lastWarnODL = adesso;
            lastCurrent = adesso;
            lgTrace($"Aggiornato IobOpcUa.lastCurrent: {lastCurrent}");
            opcUaParams = IOBConfFull.Special.OpcUaConf;
            setupMemMap();
            setupOptMemPar();
            setupOptMemPar();
            setupFileDecod();

            // gestione multi: init del contapezzi...
            if (opcUaParams.isMulti)
            {
                // init dizionari contapezz
                DictPzCountMes = new Dictionary<string, int>();
                DictPzCountImp = new Dictionary<string, int>();
                // inizializzo a zero con le tavole configurate...
                if (opcUaParams.keyPartCountTav != null && opcUaParams.keyPartCountTav.Count > 0)
                {
                    UpdatePzCountMes(opcUaParams.keyPartCountTav);
                }
            }
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// Processo i task richiesti e li elimino dalla coda 1:1
        /// </summary>
        /// <param name="task2exe"></param>
        public override Dictionary<string, string> executeTasks(Dictionary<string, string> task2exe, string codTav)
        {
            lgInfo($"OpcUa: call executeTasks | {task2exe.Count} task");
            // uso metodo base x ora
            return base.executeTasks(task2exe, codTav);
        }

        /// <summary>
        /// Effettua conversione tra gli allarmi attivi secondo configurazione banco allarmi
        /// </summary>
        /// <param name="memConf">Configurazione banco allarmi</param>
        /// <param name="numAlarms">num allarmi attivi</param>
        /// <returns></returns>
        public override string getAlarmState(BaseAlarmConf memConf, int numAlarms)
        {
            string valTransl = "";
            List<string> descAttivi = new List<string>();
            // dato il num di allarmi leggo i vari messaggi...
            int numRead = numAlarms <= memConf.activeKeys.Count() ? numAlarms : memConf.activeKeys.Count();
            for (int i = 0; i < numAlarms; i++)
            {
                descAttivi.Add(getDataItemValue(memConf.activeKeys[i]));
            }
            // combino string
            valTransl = string.Join(",", descAttivi);
            return valTransl;
        }

        /// <summary>
        /// Recupera uno specifico dataItem
        /// </summary>
        /// <param name="diKey"></param>
        /// <returns></returns>
        public string getDataItemValue(string diKey)
        {
            int currCheckErrors = 0;
            string answ = "";
            if (string.IsNullOrEmpty(diKey))
            {
                lgError($"Attenzione: richiesta chiave vuota in getDataItemValue{Environment.NewLine}StackTrace: {Environment.StackTrace}");
            }
            else
            {
                DateTime adesso = DateTime.Now;
                if (dataItemMem.Count == 0)
                {
                    currCheckErrors++;
                    lgTrace($"Errore in getDataItemValue per {diKey} | dataItemMem NON contiene valori");
                    if (vetoCheckStatus < adesso)
                    {
                        lgError($"Errore in getDataItemValue per {diKey} | dataItemMem NON contiene valori");
                        // imposto veto per vetoSeconds...
                        vetoCheckStatus = adesso.AddSeconds(vetoSeconds * 2);
                    }
                }
                else
                {
                    if (diKey.Contains(opcUaParams.kvDelim))
                    {
                        int iPos = diKey.IndexOf(opcUaParams.kvDelim);
                        string kRef = diKey.Substring(0, iPos);
                        string varDef = diKey.Substring(iPos + 1);
                        // recupero byte
                        byte[] bArray = getDataItemValueBytes(kRef);
                        answ = ConvFromBArr(bArray, varDef, opcUaParams.memDelim);
                    }
                    else
                    {
                        if (!dataItemMem.ContainsKey(diKey))
                        {
                            if (vetoCheckStatus < adesso)
                            {
                                lgError($"Errore in getDataItemValue per {diKey} | dataItemMem non contiene la chiave richiesta ma altri {dataItemMem.Count} valori");
                                lgTrace($"Esempio valori (first-last): ");
                                lgTrace($"{dataItemMem.First().Key}");
                                lgTrace($"{dataItemMem.Last().Key}");
                                // imposto veto per vetoSeconds...
                                vetoCheckStatus = adesso.AddSeconds(vetoSeconds * 2);
                            }
                            // provo a riaggiungere... rivedere...
#if false
                            int dSamplePeriod = 0;
                            float threshDBand = 0;
                            string uuid = "";
                            // formatto
                            OpcUaDataItemExt newItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, item);
                            currCheckErrors++;
                            // sistemo valore/periodo
                            newItem.value = $"";
                            newItem.valueTimestamp = adesso;
                            // aggiungo
                            dataItemMem.Add(uuid, newItem);
                            sendDataItemListToServer(dataItem.StartNodeId);
#endif
                        }
                        else
                        {
                            try
                            {
                                var currDataItem = dataItemMem[diKey];
                                answ = currDataItem.value;
                            }
                            catch (Exception exc)
                            {
                                lgError($"Errore in getDataItemValue per {diKey} | dataItemMem contiene {dataItemMem.Count} valori {Environment.NewLine}{exc}");
                                if (dataItemMem != null)
                                {
                                    lgError($"dataItemMem contiene {dataItemMem.Count} valori");
                                    int maxNum = 5;
                                    foreach (var item in dataItemMem)
                                    {
                                        maxNum--;
                                        if (maxNum < 0)
                                        {
                                            break;
                                        }
                                        lgInfo($"{item.Key} --> {item.Value.DisplayName} = {item.Value.value}");
                                    }
                                }
                            }
                        }
                    }
                }
                // se ho errori sommo, sennò abbasso numero check...
                if (currCheckErrors > 0)
                {
                    numErroriCheck += currCheckErrors;
                    lgDebug($"currCheckErrors: {currCheckErrors} | numErroriCheck: {numErroriCheck}");
                }
                else
                {
                    //numErroriCheck = numErroriCheck > 5 ? numErroriCheck-=5 : 0;
                    //numErroriCheck = numErroriCheck > 5 ? numErroriCheck-=5 : 0;
                    numErroriCheck = numErroriCheck > 0 ? numErroriCheck-- : 0;
                }
                // se supero soglia errori lettura --> disconnetto e resetto
                if (numErroriCheck > maxErroriCheck)
                {
                    lgInfo($"numErroriCheck: {numErroriCheck} --> richiesta disconnessione adapter con tryDisconnect");

                    numErroriCheck = 0;
                    tryDisconnect();
                }
            }

            return answ;
        }

        /// <summary>
        /// Recupera valore OPC-UA di un singolo byte da un byte[]
        /// </summary>
        /// <param name="diKey"></param>
        /// <param name="bitNum"></param>
        /// <param name="vTrue">Valore da restituire se true, default "1"</param>
        /// <param name="vFalse">Valore da restituire se false, default "0"</param>
        /// <returns>Restituisce string "1" / "0" secondo true/false</returns>
        public string getDataItemValueBit(string diKey, int bitNum, string vTrue = "1", string vFalse = "0")
        {
            string answ = "";
            byte[] rawValue = new byte[1];
            bool testBit = false;
            int currCheckErrors = 0;
            if (string.IsNullOrEmpty(diKey))
            {
                lgError($"Attenzione: richiesta chiave vuota in getDataItemValueBit{Environment.NewLine}StackTrace: {Environment.StackTrace}");
            }
            else
            {
                DateTime adesso = DateTime.Now;
                if (dataItemMem.Count == 0)
                {
                    currCheckErrors++;
                    lgTrace($"Errore in getDataItemValueBit per {diKey} | dataItemMem NON contiene valori");
                    if (vetoCheckStatus < adesso)
                    {
                        lgError($"Errore in getDataItemValueBit per {diKey} | dataItemMem NON contiene valori");
                        // imposto veto per vetoSeconds...
                        vetoCheckStatus = adesso.AddSeconds(vetoSeconds * 2);
                    }
                }
                else
                {
                    if (!dataItemMem.ContainsKey(diKey))
                    {
                        if (vetoCheckStatus < adesso)
                        {
                            lgError($"Errore in getDataItemValueBit per {diKey} | dataItemMem non contiene la chiave richiesta ma altri {dataItemMem.Count} valori");
                            lgTrace($"Esempio valori (first-last): ");
                            lgTrace($"{dataItemMem.First().Key}");
                            lgTrace($"{dataItemMem.Last().Key}");
                            // imposto veto per vetoSeconds...
                            vetoCheckStatus = adesso.AddSeconds(vetoSeconds * 2);
                        }
                        currCheckErrors++;
                    }
                    else
                    {
                        try
                        {
                            // MAIN TEST
                            var currDataItem = dataItemMem[diKey];
                            rawValue = currDataItem.rawByte;
                            if (rawValue.Length > bitNum)
                            {
                                testBit = rawValue[bitNum] == 1;
                            }
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore in getDataItemValueBit per {diKey} | dataItemMem contiene {dataItemMem.Count} valori {Environment.NewLine}{exc}");
                            if (dataItemMem != null)
                            {
                                lgError($"dataItemMem contiene {dataItemMem.Count} valori");
                                int maxNum = 5;
                                foreach (var item in dataItemMem)
                                {
                                    maxNum--;
                                    if (maxNum < 0)
                                    {
                                        break;
                                    }
                                    lgInfo($"{item.Key} --> {item.Value.DisplayName} = {item.Value.value}");
                                }
                            }
                        }
                    }
                }
                // se ho errori sommo, sennò abbasso numero check...
                if (currCheckErrors > 0)
                {
                    numErroriCheck += currCheckErrors;
                    lgDebug($"currCheckErrors: {currCheckErrors} | numErroriCheck: {numErroriCheck}");
                }
                else
                {
                    numErroriCheck = numErroriCheck > 0 ? numErroriCheck-- : 0;
                }
                // se supero soglia errori lettura --> disconnetto e resetto
                if (numErroriCheck > maxErroriCheck)
                {
                    lgInfo($"numErroriCheck: {numErroriCheck} --> richiesta disconnessione adapter con tryDisconnect");

                    numErroriCheck = 0;
                    tryDisconnect();
                }
            }
            answ = testBit ? vTrue : vFalse;
            return answ;
        }

        /// <summary>
        /// Recupera valore OPC-UA come array di byte[] x successivo processing
        /// </summary>
        /// <param name="diKey">Chiave da recuperare</param>
        /// <returns>Restituisce string "1" / "0" secondo true/false</returns>
        public byte[] getDataItemValueBytes(string diKey)
        {
            byte[] answ = new byte[1];
            byte[] rawValue = new byte[8];
            int currCheckErrors = 0;
            if (string.IsNullOrEmpty(diKey))
            {
                lgError($"Attenzione: richiesta chiave vuota in getDataItemValueBytes{Environment.NewLine}StackTrace: {Environment.StackTrace}");
            }
            else
            {
                DateTime adesso = DateTime.Now;
                if (dataItemMem.Count == 0)
                {
                    currCheckErrors++;
                    lgTrace($"Errore in getDataItemValueBytes per {diKey} | dataItemMem NON contiene valori");
                    if (vetoCheckStatus < adesso)
                    {
                        lgError($"Errore in getDataItemValueBytes per {diKey} | dataItemMem NON contiene valori");
                        // imposto veto per vetoSeconds...
                        vetoCheckStatus = adesso.AddSeconds(vetoSeconds * 2);
                    }
                }
                else
                {
                    if (!dataItemMem.ContainsKey(diKey))
                    {
                        if (vetoCheckStatus < adesso)
                        {
                            lgError($"Errore in getDataItemValueBytes per {diKey} | dataItemMem non contiene la chiave richiesta ma altri {dataItemMem.Count} valori");
                            lgTrace($"Esempio valori (first-last): ");
                            lgTrace($"{dataItemMem.First().Key}");
                            lgTrace($"{dataItemMem.Last().Key}");
                            // imposto veto per vetoSeconds...
                            vetoCheckStatus = adesso.AddSeconds(vetoSeconds * 2);
                        }
                        currCheckErrors++;
                    }
                    else
                    {
                        try
                        {
                            // MAIN TEST
                            var currDataItem = dataItemMem[diKey];
                            answ = currDataItem.rawByte;
                        }
                        catch (Exception exc)
                        {
                            lgError($"Errore in getDataItemValueBytes per {diKey} | dataItemMem contiene {dataItemMem.Count} valori {Environment.NewLine}{exc}");
                            if (dataItemMem != null)
                            {
                                lgError($"dataItemMem contiene {dataItemMem.Count} valori");
                                int maxNum = 5;
                                foreach (var item in dataItemMem)
                                {
                                    maxNum--;
                                    if (maxNum < 0)
                                    {
                                        break;
                                    }
                                    lgInfo($"{item.Key} --> {item.Value.DisplayName} = {item.Value.value}");
                                }
                            }
                        }
                    }
                }
                // se ho errori sommo, sennò abbasso numero check...
                if (currCheckErrors > 0)
                {
                    numErroriCheck += currCheckErrors;
                    lgDebug($"currCheckErrors: {currCheckErrors} | numErroriCheck: {numErroriCheck}");
                }
                else
                {
                    numErroriCheck = numErroriCheck > 0 ? numErroriCheck-- : 0;
                }
                // se supero soglia errori lettura --> disconnetto e resetto
                if (numErroriCheck > maxErroriCheck)
                {
                    lgInfo($"numErroriCheck: {numErroriCheck} --> richiesta disconnessione adapter con tryDisconnect");

                    numErroriCheck = 0;
                    tryDisconnect();
                }
            }
            return answ;
        }

        /// <summary>
        /// Recupera valore OPC-UA direttamente in formato double
        /// </summary>
        /// <param name="diKey"></param>
        /// <returns></returns>
        public double getDataItemValueDouble(string diKey)
        {
            double answ = 0;
            string rawVal = getDataItemValue(diKey);
            if (rawVal != null)
            {
                double.TryParse(diKey, out answ);
            }
            return answ;
        }

        /// <summary>
        /// Recupero dati dinamici...
        /// </summary>
        public override Dictionary<string, string> getDynData()
        {
            Dictionary<string, string> outVal = new Dictionary<string, string>();
            return outVal;
        }

        /// <summary>
        /// Recupero programma in lavorazione
        /// </summary>
        /// <returns></returns>
        public override string getPrgName()
        {
            string prgName = "";
            if (!string.IsNullOrEmpty(opcUaParams.keyProgName))
            {
                prgName = getDataItemValue(opcUaParams.keyProgName);
            }
            return prgName;
        }

        /// <summary>
        /// Effettua vero processing contapezzi
        /// </summary>
        public override void processContapezzi()
        {
            if (enablePzCountByApp)
            {
                // verifico conf macchina stadard / MULTI
                if (!opcUaParams.isMulti)
                {
                    processContapezziSingle();
                }
                else
                {
                    processContapezziMulti();
                }
            }
        }

        /// <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);
            try
            {
                if (verboseLog)
                {
                    lgInfo("inizio read semafori");
                }

                currDispData.semIn = Semaforo.SV;

                // verifico SE sia necessario forzare la lettura RAW
                if (doByteRead)
                {
                    if (DateTime.Now.Subtract(lastCurrent).TotalSeconds > MaxSecReload)
                    {
                        // FIXME TODO !!!
                        foreach (var item in dataItemMem)
                        {
                            try
                            {
                                // restituisce i 115 byte da deserializzare... qui HACK!
                                var rawVal = UA_ref.ReadNodeRaw(item.Value.StartNodeId);
                                if (rawVal != null)
                                {
                                    byteRawData = getByteRaw((DataValue)rawVal);
                                    lgTrace($"Aggiornato readSemafori.lastCurrent: {lastCurrent}");
                                    currReadErrors = 0;
                                }
                                else
                                {
                                    currReadErrors++;
                                }
                            }
                            catch (Exception exc)
                            {
                                lgError($"Eccezione durante UA_ref.ReadNodeRaw per {item.Value.StartNodeId}{Environment.NewLine}{exc}");
                                currReadErrors++;
                            }
                        }
                    }
                }

                // decodifica e gestione
                decodeToBaseBitmap();
                reportRawInput(ref currDispData);
                lastCurrent = DateTime.Now;
            }
            catch (Exception exc)
            {
                currDispData.semIn = Semaforo.SR;
                lgError($"Eccezione in readSemafori:{Environment.NewLine}{exc}");
            }
            // se > max errori --> disconnetto
            if (currReadErrors > maxReadErrors)
            {
                lgError($"Superato limite errori Read ({currReadErrors}) --> tryDisconnect");
                currReadErrors = 0;
                tryDisconnect();
            }
#if true
            else
            {
                // altrimenti pausa forzata
                Thread.Sleep(300);
            }
#endif
        }

        /// <summary>
        /// Effettua reset del contapezzi, NON POSSIBILE in questa versione
        /// </summary>
        /// <returns></returns>
        public override bool resetContapezziPLC(string codTav)
        {
            lgInfo("OpcUa.resetContapezziPLC - none");
            bool answ = false;
            return answ;
        }

        /// <summary>
        /// Effettua IMPOSTAZIONE FORZATA del contapezzi, NON POSSIBILE in questa versione
        /// </summary>
        /// <returns></returns>
        public override bool setcontapezziPLC(int newPzCount, string codTav)
        {
            bool answ = false;
            return answ;
        }

        /// <summary>
        /// Override connessione
        /// </summary>
        public override void tryConnect()
        {
            if (!connectionOk)
            {
                // controllo che il ping sia stato tentato almeno pingTestSec fa...
                if (DateTime.Now.Subtract(lastPING).TotalSeconds > utils.CRI("pingTestSec"))
                {
                    if (verboseLog || periodicLog)
                    {
                        lgInfo("OpcUa: ConnKO - tryConnect");
                    }
                    // in primis salvo data ping...
                    lastPING = DateTime.Now;
                    // se passa il ping faccio il resto...
                    if (testPingMachine == IPStatus.Success || opcUaParams.forcePingOk)
                    {
                        string szStatusConnection = "";
                        try
                        {
                            // ora provo connessione...
                            parentForm.commPlcActive = true;
                            var task = Task.Run(async () =>
                            {
                                return await doConnect();
                                //return await doConnect().ConfigureAwait(false);
                            });
                            short esitoLink = task.Result; // use returned result from async method here
                            lgInfo($"szStatusConnection OpcUa, esitoLink: {esitoLink}");
                            parentForm.commPlcActive = false;
                            // connesso se esito == 1...
                            connectionOk = (esitoLink == 1);
                            // refresh stato allarmi!!!
                            if (connectionOk)
                            {
                                queueInEnabCurr = true;
                                if (adpRunning)
                                {
                                    lgInfo("Connessione OK");
                                }
                            }
                            else
                            {
                                lgError("Impossibile procedere, connessione mancante...");
                            }
                        }
                        catch (Exception exc)
                        {
                            lgFatal($"Errore nella connessione all'adapter OpcUa: {szStatusConnection}{Environment.NewLine}{exc}");
                            connectionOk = false;
                            lgInfo($"Eccezione in TryConnect, Adapter OpcUa NON running, pausa di {utils.CRI("waitRecMSec")} msec prima di ulteriori tentativi di riconnessione");
                        }
                    }
                    else
                    {
                        // loggo no risposta ping ...
                        connectionOk = false;
                        if (verboseLog || periodicLog)
                        {
                            lgInfo($"Attenzione: OpcUa controllo PING fallito per IP {IOBConfFull.Device.Connect.PingIpAddr}");
                        }
                    }
                }
            }
            else
            {
                needRefresh = true;
            }
            // se non è ancora connesso faccio procesisng memoria caso disconnesso...
            if (!connectionOk)
            {
                // processo semafori ed invio...
                processMemoryDiscon();
            }
        }

        /// <summary>
        /// Override disconnessione
        /// </summary>
        public override void tryDisconnect()
        {
            if (connectionOk)
            {
                if (UA_ref != null)
                {
                    string szStatusConnection = "";
                    try
                    {
                        UA_ref.Disconnect();
                        connectionOk = false;
                        lgInfo(szStatusConnection);
                        lgInfo("Effettuata disconnessione adapter OpcUa!");
                    }
                    catch (Exception exc)
                    {
                        lgFatal(exc, "Errore nella disconnessione dall'adapter OpcUa");
                    }
                }
                else
                {
                    lgDebug("IMPOSSIBILE effettuare disconnessione OpcUa: UA_ref non disponibile...");
                }
            }
            else
            {
                lgDebug("IMPOSSIBILE effettuare disconnessione OpcUa: Connessione non disponibile...");
            }
            queueInEnabCurr = false;
        }

        #endregion Public Methods

        #region Internal Properties

        /// <summary>
        /// Dizionario chiavi OpcUa bonificate da char non standard
        /// </summary>
        internal Dictionary<string, string> DictOpcFixKeys { get; set; } = new Dictionary<string, string>();

        #endregion Internal Properties

        #region Internal Methods

        /// <summary>
        /// Verifica ed invia variazioni
        /// </summary>
        /// <param name="MonIt"></param>
        /// <param name="NotifyValue"></param>
        /// <param name="forceSend"></param>
        internal bool checkAndSend(Opc.Ua.Client.MonitoredItem MonIt, string NotifyValue, bool forceSend)
        {
            bool changed = false;
            bool hasVetoFL = false;
            bool hasVetoLK = false;
            bool hasVetoLKV = false;
            if (MonIt != null)
            {
                string uuid = calcID($"{MonIt.StartNodeId.Identifier}", $"{MonIt.DisplayName}");
                if (!string.IsNullOrEmpty(NotifyValue))
                {
                    string sVal = "";
                    string descr = "";
                    DateTime locTStamp = DateTime.Now;

                    descr = itemTranslation("OPC", uuid);
                    sVal = $"Test Change 01: {locTStamp.ToString()} | descr: {descr} | Val: {NotifyValue}";
                    //sVal = $"Change 01: {locTStamp.ToString()} | descr: {descr} | Id: {MonIt.StartNodeId} | Val: {NotifyValue}";
                    // cerco se non sia un dato filtrato x invio FLUXLOG in toto...
                    hasVetoFL = opcUaParams.fluxLogVeto.Contains(uuid);
                    // cerco filtro ANCHE PER solo display name x contains...
                    if (!hasVetoFL)
                    {
                        hasVetoFL = opcUaParams.fluxLogVetoContains.Contains(uuid) || opcUaParams.fluxLogVetoContains.Contains(MonIt.DisplayName);
                    }
                    if (hasVetoFL)
                    {
                        lgTrace($"NON ACCODATO sample per {uuid} - trovato VETO in fluxLogVeto", false);
                    }
                    else
                    {
                        // verifico se salvare variazione (in LUT locale)
                        changed = checkSaveValue(MonIt, NotifyValue, false);
                        // cerco se sia un dato/valore
                        hasVetoLK = opcUaParams.fluxLogKeyValVeto.ContainsKey(descr);
                        if (hasVetoLK)
                        {
                            // se c'è controllo il valore...
                            var valList = opcUaParams.fluxLogKeyValVeto[descr];
                            hasVetoLKV = valList.Contains(NotifyValue);
                        }
                        if (hasVetoLKV)
                        {
                            lgTrace($"NON ACCODATO sample: veto trovato per {descr}/{NotifyValue} in fluxLogKeyValVeto", false);
                        }
                        else
                        {
                            if (changed || forceSend)
                            {
                                bool sent = accodaFLog(descr, sVal, qEncodeFLog(descr, NotifyValue));
                                if (sent)
                                {    // traccio valore DynData x analisi
                                    trackDynData(descr, NotifyValue);
                                }
                            }
                            else
                            {
                                lgTrace($"NON ACCODATO sample per {uuid} - verifica variazione ha dato esito negativo", false);
                            }
                        }
                    }
                }
                else
                {
                    lgDebug($"checkAndSend ERROR 01 | MonIt: {uuid} | NotifyValue Null!!!");
                }
            }
            else
            {
                lgError("checkAndSend ERROR: MonIt null");
            }
            // salvataggio finale registrazione valori ricevuti...
            try
            {
                // salvo dati scambiati
                trackExchData(Encoding.UTF8.GetBytes(NotifyValue).Length);
            }
            catch
            {
                lgError("checkAndSend ERROR: trackExchData");
            }
            return changed;
        }

        /// <summary>
        /// Verifica ed invia variazioni DAL FORMATO RAW data (byte[])
        /// </summary>
        /// <param name="MonIt"></param>
        /// <param name="NotifyValue"></param>
        /// <param name="forceSend"></param>
        internal virtual bool checkAndSendRaw(Opc.Ua.Client.MonitoredItem MonIt, byte[] NotifyValue, bool forceSend)
        {
            bool changed = false;
            if (MonIt != null)
            {
                string uuid = calcID($"{MonIt.StartNodeId.Identifier}", $"{MonIt.DisplayName}");
                if (NotifyValue != null && NotifyValue.Length > 0)
                {
                    // versione base: il valore è la stringa composta da TUTTI i valori in BYTE
                    // espressi come comma-sep-string
                    StringBuilder sb = new StringBuilder();
                    foreach (var bVal in NotifyValue)
                    {
                        sb.Append($"{bVal},");
                    }
                    string currVal = sb.ToString();
                    // tolgo ultima string
                    if (currVal.Length > 1)
                    {
                        currVal = currVal.Substring(0, currVal.Length - 1);
                    }

                    string sVal = "";
                    string descr = "";
                    DateTime locTStamp = DateTime.Now;
                    descr = itemTranslation("OPC", uuid);
                    // calcolo nomi "bonificati"
                    string startNodeId = stringFixOpcUaName($"{MonIt.StartNodeId}");
                    string displayName = stringFixOpcUaName($"{MonIt.DisplayName}");
                    sVal = $"Change 02: {locTStamp.ToString()} | descr: {descr} | Id: {startNodeId} | Val: {currVal}";
                    lgDebug(sVal);

                    // verifico se salvare
                    changed = checkSaveValue(MonIt, currVal, NotifyValue, true) || forceSend;
                    // cerco se non sia un dato filtrato in FLUXLOG...
                    bool isFiltered = opcUaParams.fluxLogVeto.Contains(uuid);
                    // cerco filtro ANCHE PER solo display name x contains...
                    if (!isFiltered)
                    {
                        isFiltered = opcUaParams.fluxLogVetoContains.Contains(uuid) || opcUaParams.fluxLogVetoContains.Contains(displayName);
                    }
                    if (isFiltered)
                    {
                        lgTrace($"NON ACCODATO sample per {uuid} - trovato VETO in fluxLogVeto", false);
                    }
                    else
                    {
                        if (changed || forceSend)
                        {
                            //accodaFLog(sVal, qEncodeFLog(descr, $"{NotifyValue}"));
                            bool parentSent = false;
                            // invia SOLO SE i dati raw sono configurati x invio...
                            if (opcUaParams.sendSubscribItemsRaw)
                            {
                                bool sent = accodaFLog(descr, sVal, qEncodeFLog(descr, currVal));
                                parentSent = true;
                                if (sent)
                                {    // traccio valore DynData x analisi
                                    trackDynData(descr, currVal);
                                }
                            }

                            // se abilitata decodifica area mMapRead da byte...
                            if (opcUaParams.mMapReadFromRawByte)
                            {
                                // calcolo eventuali variazioni dati "collegati" in memRead/memWrite...
                                if (memMap.mMapRead != null && memMap.mMapRead.Count > 0)
                                {
                                    // verifica se il dato "parent" è esplicitamente abilitato x invio...
                                    var parentRev = memMap.mMapRead.Where(x => x.Value.memAddr.Equals(uuid)).ToList();
                                    if (!parentSent && parentRev.Count == 1)
                                    {
                                        // verifico invio abilitato
                                        if (sendEnabledFLog(uuid))
                                        {
                                            bool sent = accodaFLog(descr, sVal, qEncodeFLog(descr, currVal));
                                            parentSent = true;
                                            if (sent)
                                            {    // traccio valore DynData x analisi
                                                trackDynData(descr, currVal);
                                            }
                                        }
                                    }
                                    // elenco aree collegate...
                                    var listRelated = memMap.mMapRead.Where(x => x.Value.memAddr.Contains(uuid)).ToList();
                                    if (listRelated != null && listRelated.Count() > 0)
                                    {
                                        foreach (var item in listRelated)
                                        {
                                            // verifico invio abilitato
                                            if (sendEnabledFLog(item.Key))
                                            {
                                                // ciclo e calcolo valore
                                                var rByte = dataItemMem[uuid].rawByte;
                                                int iPos = item.Value.memAddr.IndexOf(opcUaParams.kvDelim);
                                                string varDef = item.Value.memAddr.Substring(iPos + 1);
                                                string convVal = ConvFromBArr(rByte, varDef, opcUaParams.memDelim);
                                                // verifica SE ci sia deadBand da validare...
                                                if (valueOverDBand(item.Key, convVal))
                                                {
                                                    // metto in array memData...
                                                    if (upsertKey(item.Key, convVal))
                                                    {
                                                        // accodo se variato!
                                                        bool sent = accodaFLog(item.Key, $"mMapRead | {item.Key} | {convVal}", qEncodeFLog(item.Key, convVal));
                                                        if (sent)
                                                        {    // traccio valore DynData x analisi
                                                            trackDynData(item.Key, convVal);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            // se abilitata decodifica area mMapWrite da byte...
                            if (opcUaParams.mMapWriteFromRawByte)
                            {
                                if (memMap.mMapWrite != null && memMap.mMapWrite.Count > 0)
                                {
                                    // elenco aree collegate...
                                    var listRelated = memMap.mMapWrite.Where(x => x.Value.memAddr.Contains(uuid)).ToList();
                                    if (listRelated != null && listRelated.Count() > 0)
                                    {
                                        foreach (var item in listRelated)
                                        {
                                            // verifico invio abilitato
                                            if (sendEnabledFLog(item.Key))
                                            {
                                                // ciclo e calcolo valore
                                                var rByte = dataItemMem[uuid].rawByte;
                                                int iPos = item.Value.memAddr.IndexOf(opcUaParams.kvDelim);
                                                string varDef = item.Value.memAddr.Substring(iPos + 1);
                                                string convVal = ConvFromBArr(rByte, varDef, opcUaParams.memDelim);
                                                // verifica SE ci sia deadBand da validare...
                                                if (valueOverDBand(item.Key, convVal))
                                                {
                                                    // metto in array memData...
                                                    if (upsertKey(item.Key, convVal))
                                                    {
                                                        // accodo se variato!
                                                        bool sent = accodaFLog(item.Key, $"mMapRead | {item.Key} | {convVal}", qEncodeFLog(item.Key, convVal));
                                                        if (sent)
                                                        {    // traccio valore DynData x analisi
                                                            trackDynData(item.Key, convVal);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        else
                        {
                            lgTrace($"NON ACCODATO sample per {uuid} - verifica variazione ha dato esito negativo", false);
                        }
                    }
                }
                else
                {
                    lgError($"checkAndSend ERROR 02 | MonIt: {uuid} | NotifyValue Null!!!");
                }
            }
            else
            {
                lgError("checkAndSend ERROR: MonIt null");
            }
            // salvataggio finale registrazione valori ricevuti...
            try
            {
                // salvo dati scambiati
                trackExchData(NotifyValue.Length);
            }
            catch
            {
                lgError("checkAndSend ERROR: trackExchData");
            }
            return changed;
        }

        internal void sendDataItemListToServer(NodeId StartNodeId)
        {
            // converto gli attuali nell'elenco dataitem...
            List<machDataItem> elencoDataItems = dataItemMem.Select(d => new machDataItem()
            {
                uuid = d.Key,
                Category = DataItemCategory.EVENT,
                Name = d.Value.DisplayName,
                Type = $"{d.Value.NodeClass}",
                SubType = $"{StartNodeId}"
            }
            ).ToList();

            lgInfo($"Richiesta sendDataItemListToServer per {elencoDataItems.Count} dataItems");
            // aspetta un tempo random da 10-100 ms...
            Random rnd = new Random();
            Task.Delay(rnd.Next(10, 100));
            // invio il dataItem serializzato...
            sendDataItemsList(elencoDataItems);
        }

        /// <summary>
        /// Fix stringa da versione "raw " a "bonificata"
        /// </summary>
        /// <param name="origName"></param>
        /// <returns></returns>
        internal string stringFixOpcUaName(string origName)
        {
            string answ = origName;
            // se ho dizionari di sostituzione caratteri sistemo!
            if (opcUaParams.DictOpcNameReplace.Count > 0)
            {
                // cerco nel dizionario LUT di traduzione
                if (DictOpcFixKeys.ContainsKey(origName))
                {
                    answ = DictOpcFixKeys[origName];
                }
                else
                {
                    // altrimenti sitemo e metto in dizionario
                    foreach (var item in opcUaParams.DictOpcNameReplace)
                    {
                        answ = answ.Replace(item.Key, item.Value);
                    }
                    DictOpcFixKeys.Add(origName, answ);
                }
            }
            return answ;
        }

        /// <summary>
        /// Evento rilevazione modifica valori effettua checkSend
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        internal virtual void UA_ref_eh_MonItChange(object sender, opcUaMonitItemChange e)
        {
            lgTrace("Richiamato UA_ref_eh_MonItChange");
            string currVal = "";
            // da verificare decodifica valore byte...
            if (doByteRead)
            {
                var currNot = e.CurrNotify;
                if (currNot != null)
                {
                    byteRawData = getByteRaw((DataValue)currNot.Value);
                    checkAndSendRaw(e.CurrMonitoredItem, byteRawData, false);
                }
            }
            else
            {
                currVal = $"{e.CurrNotify.Value}";
                bool hasChanges = checkAndSend(e.CurrMonitoredItem, currVal, false);
                if (hasChanges)
                {
                    FluxMemSave();
                }
            }
            // aggiorno ultima lettura
            lastCurrent = DateTime.Now;
        }

        #endregion Internal Methods

        #region Protected Fields

        /// <summary>
        /// Struttura dove vengono memorizzati i dataitem ed i rispettivi valori x processing
        /// </summary>
        protected Dictionary<string, OpcUaDataItemExt> dataItemMem = new Dictionary<string, OpcUaDataItemExt>();

        /// <summary>
        /// Gestione filtraggio dati
        /// </summary>
        protected bool enableDataFilter = false;

        /// <summary>
        /// Dizionario dei valori di FluxLog con relativi valori
        /// </summary>
        protected Dictionary<string, FluxData> FluxMem = new Dictionary<string, FluxData>();

        /// <summary>
        /// Determina se ha effettuata lettura items in memoria x confronto...
        /// </summary>
        protected bool hasReadItems = false;

        /// <summary>
        /// Ultimo current received x gestione update periodico...
        /// </summary>
        protected DateTime lastCurrent = DateTime.Now;

        /// <summary>
        /// Oggetto MAIN x connessione OPC-UA
        /// </summary>
        protected UAClient UA_ref;

        /// <summary>
        /// Veto controllo status x log...
        /// </summary>
        protected DateTime vetoCheckStatus = DateTime.Now;

        protected int WatchDog = 0;

        #endregion Protected Fields

        #region Protected Properties

        /// <summary>
        /// Area dati raw per lettura encoded (SE presente)
        /// </summary>
        protected byte[] byteRawData { get; set; } = new byte[1];

        /// <summary>
        /// ProgName corrente
        /// </summary>
        protected string currProgName
        {
            get
            {
                string answ = "";
                if (!string.IsNullOrEmpty(opcUaParams.keyProgName))
                {
                    answ = getDataItemValue(opcUaParams.keyProgName);
                }
                return answ;
            }
        }

        /// <summary>
        /// Indica se si debba leggere un area di tipo "encoded" (byte raw --&gt; obj)
        /// </summary>
        protected bool doByteRead { get; set; } = false;

        /// <summary>
        /// Verifico se abbia TAV 1 attiva...
        /// </summary>
        protected bool hasActiveTav1
        {
            get
            {
                return checkMultiCondition(opcUaParams.condActTav1);
            }
        }

        /// <summary>
        /// Verifico se abbia TAV 2 attiva...
        /// </summary>
        protected bool hasActiveTav2
        {
            get
            {
                return checkMultiCondition(opcUaParams.condActTav2);
            }
        }

        /// <summary>
        /// Verifico se abbia ALMENO un errore...
        /// </summary>
        protected bool hasError
        {
            get
            {
                return checkMultiCondition(opcUaParams.condError);
            }
        }

        /// <summary>
        /// Indica se abbia emergenza ARMATA (cond normale)
        /// </summary>
        protected bool hasEStopArmed
        {
            get
            {
                return checkMultiCondition(opcUaParams.condEStop);
            }
        }

        /// <summary>
        /// Indica se abbia stato POWER ON (multicondizione)
        /// </summary>
        protected virtual bool hasPowerOn
        {
            get
            {
                return checkMultiCondition(opcUaParams.condPowerOn);
            }
        }

        /// <summary>
        /// Indica se abbia stato MANUAL (condizioni varie, es stopped)
        /// </summary>
        protected virtual bool isManual
        {
            get
            {
                return checkMultiCondition(opcUaParams.condManual);
            }
        }

        /// <summary>
        /// Indica se abbia stato READY (condizioni varie, es ausiliari OK)
        /// </summary>
        protected virtual bool isReady
        {
            get
            {
                return checkMultiCondition(opcUaParams.condReady);
            }
        }

        /// <summary>
        /// Indica se sia in stato Ssetup
        /// </summary>
        protected bool isSetup
        {
            get
            {
                return checkMultiCondition(opcUaParams.condSetup);
            }
        }

        /// <summary>
        /// Indica se sia in stato WarmUp / CoolDown (riscaldamento/raffreddamento)
        /// </summary>
        protected bool isWarmUpCoolDown
        {
            get
            {
                return checkMultiCondition(opcUaParams.condWarmUpCoolDown);
            }
        }

        /// <summary>
        /// Indica se sia in stato Warning
        /// </summary>
        protected bool isWarning
        {
            get
            {
                return checkMultiCondition(opcUaParams.condWarning);
            }
        }

        /// <summary>
        /// Indica se abbia stato READY (condizioni varie, es ausiliari OK)
        /// </summary>
        protected bool isWorking
        {
            get
            {
                // cerco SE HO cond OPC Ua o classica... nel caso creo e salvo
                if (opcUaParams.condWorkOpc.checkList == null || opcUaParams.condWorkOpc.checkList.Count == 0)
                {
                    opcUaParams.condWorkOpc = new diCheckCondSetup()
                    {
                        checkList = opcUaParams.condWork,
                        checkMode = boolCheckMode.AND,
                        negateValue = false
                    };
                }
                return checkMultiCondition(opcUaParams.condWorkOpc);
            }
        }

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

        /// <summary>
        /// Parametri specifici OPC-UA
        /// </summary>
        protected OpcUaParamConf opcUaParams { get; set; }

        /// <summary>
        /// item sottoscritti x lettura periodica in caso di dati "lentamente varianti"....
        /// </summary>
        protected List<Opc.Ua.Client.MonitoredItem> subscribedItems { get; set; } = new List<Opc.Ua.Client.MonitoredItem>();

        #endregion Protected Properties

        #region Protected Methods

        /// <summary>
        /// Calcola UUID per i vari items data configurazione OPC-UA corrente
        /// </summary>
        /// <param name="fullCode">Codice univoco completo (tipicamente quello che viene dopo ns=2;s=)</param>
        /// <param name="shortCode">Codice leaf finale (NON necessariamente univoco)</param>
        /// <returns></returns>
        protected string calcID(string fullCode, string shortCode)
        {
            // parto assegnando il valore "short" che è il vecchio default...
            string answ = shortCode;
            if (opcUaParams.UseFullId)
            {
                if (string.IsNullOrEmpty(opcUaParams.BaseIdMask))
                {
                    answ = fullCode;
                }
                else
                {
                    answ = fullCode.Replace(opcUaParams.BaseIdMask, "");
                }
                answ = stringFixOpcUaName(answ);
                // trimmo spazi x ridurre dimensione se > 50...
                if (answ.Length > 50)
                {
                    answ = answ.Replace(" ", "");
                }
            }
            return answ;
        }

        /// <summary>
        /// Verifica un DataItem e se il valore corrisponde a quello indicato come "true value"
        /// restituisce true
        /// </summary>
        /// <param name="itemName"></param>
        /// <param name="trueVal"></param>
        /// <returns></returns>
        protected bool checkDataItem(string itemName, string trueVal)
        {
            bool answ = false;
            OpcUaDataItemExt currValue = null;
            try
            {
                currValue = dataItemMem[itemName];
                answ = (currValue.value.Equals(trueVal));
            }
            catch
            {
                lgError($"Errore in decodifica valore per {itemName} rispetto a {trueVal} | recuperato {currValue} / {currValue.value}");
            }
            return answ;
        }

        /// <summary>
        /// Verifica se i dati siano validi/live altrimenti riavvia adapter OPC-UA
        /// </summary>
        /// <param name="adesso"></param>
        protected void CheckLiveData(DateTime adesso)
        {
            // controllo se non ho dati buoni da > MaxSecReload sec --> disconnetto
            if (adesso.Subtract(lastCurrent).TotalSeconds > MaxSecReload)
            {
                if (enableCliRestart)
                {
                    lgInfo($"Timeout per mancata comunicazione da oltre {MaxSecReload} sec --> disconnessione adapter OpcUa!");
                    tryDisconnect();
                }
                else
                {
                    lgError($"Attenzione: superato periodo di mancata comunicazione da oltre {MaxSecReload} sec ma enableCliRestart non permesso");
                }
            }
        }

        /// <summary>
        /// Verifica condizione "multipla" secondo setup json
        /// </summary>
        /// <param name="reqCondition">Set condizioni da validare</param>
        /// <returns></returns>
        protected bool checkMultiCondition(diCheckCondSetup reqCondition)
        {
            bool answ = false;
            int numCondOk = 0;
            int numCond = 0;
            if (reqCondition.checkList != null && reqCondition.checkList.Count > 0)
            {
                numCond = reqCondition.checkList.Count;
                // cerco nell'elenco delle condizioni che indicano lavora se sono ok faccio +1 conteggio......
                foreach (var item in reqCondition.checkList)
                {
                    if (string.IsNullOrEmpty(item.keyName))
                    {
                        lgError($"Attenzione: item vuoto in checkMultiCondition{Environment.NewLine}StackTrace: {Environment.StackTrace}");
                    }
                    else
                    {
                        // verifico se ci sia richiesta x test bit...
                        if (item.bitNum >= 0)
                        {
                            if (getDataItemValueBit(item.keyName, item.bitNum) == item.targetValue)
                            {
                                numCondOk++;
                            }
                        }
                        else
                        {
                            if (getDataItemValue(item.keyName).Equals(item.targetValue, StringComparison.InvariantCultureIgnoreCase))
                            {
                                numCondOk++;
                            }
                        }
                    }
                }
                if (reqCondition.checkMode == boolCheckMode.AND)
                {
                    answ = (numCond == numCondOk);
                }
                else if (reqCondition.checkMode == boolCheckMode.OR)
                {
                    answ = numCondOk > 0;
                }
            }
            else
            {
                answ = true;
            }
            // verifico se devo negare il valore...
            answ = reqCondition.negateValue ? !answ : answ;
            // restituisco
            return answ;
        }

        /// <summary>
        /// Verifica / Salva valore e restitusice SE sia variato (e quindi da inviare...)
        /// </summary>
        /// <param name="dataItem"></param>
        /// <param name="NotifyValue"></param>
        /// <param name="RawByteVal"></param>
        /// <param name="SendItemList"></param>
        /// <returns></returns>
        protected bool checkSaveValue(Opc.Ua.Client.MonitoredItem dataItem, string NotifyValue, byte[] RawByteVal, bool SendItemList)
        {
            bool answ = checkSaveValue(dataItem, NotifyValue, SendItemList);
            if (answ)
            {
                string uuid = calcID($"{dataItem.StartNodeId.Identifier}", $"{dataItem.DisplayName}");
                dataItemMem[uuid].rawByte = RawByteVal;
            }
            return answ;
        }

        /// <summary>
        /// Verifica / Salva valore e restitusice SE sia variato (e quindi da inviare...)
        /// </summary>
        /// <param name="dataItem"></param>
        /// <param name="NotifyValue"></param>
        /// <param name="SendItemList"></param>
        /// <returns></returns>
        protected bool checkSaveValue(Opc.Ua.Client.MonitoredItem dataItem, string NotifyValue, bool SendItemList)
        {
            bool answ = !enableDataFilter;
            double oldVal = 0;
            double newVal = 0;
            if (dataItem != null)
            {
                string uuid = calcID($"{dataItem.StartNodeId.Identifier}", $"{dataItem.DisplayName}");

                if (!string.IsNullOrEmpty(NotifyValue) && !NotifyValue.Contains("null"))
                {
#if DEBUG
                    //lgTrace($"Richiesta checkSaveValue per {dataItem.DisplayName} | id: {dataItem.StartNodeId} | Valore: {NotifyValue}");
#endif
                    // verifico in memoria se ho l'oggetto condition ed il suo valore..
                    DateTime adesso = DateTime.Now;
                    if (FluxMem.ContainsKey(uuid))
                    {
                        var currDataItemMem = FluxMem[uuid];
                        // controllo SE SIA scaduto il tempo massimo...
                        int sendVeto = currDataItemMem.SamplePeriod > IOBConfFull.FluxLog.SamplePeriod ? currDataItemMem.SamplePeriod : IOBConfFull.FluxLog.SamplePeriod;
                        if (Math.Abs(currDataItemMem.LastUpdated.Subtract(adesso).TotalSeconds) > sendVeto)
                        {
                            answ = true;
                        }
                        else
                        {
                            // ALTRIMENTI controllo SE diverso
                            if (currDataItemMem.Value != $"{NotifyValue}")
                            {
                                // controllo SE ho DeadBand.. forse si può evitare controllo..
                                double dBand = currDataItemMem.DeadBand > IOBConfFull.FluxLog.DeadBand ? currDataItemMem.DeadBand : IOBConfFull.FluxLog.DeadBand;
                                if (dBand > 0)
                                {
                                    // recupero i valori e testo DeadBand...
                                    bool isNum01 = double.TryParse(currDataItemMem.Value.Replace(".", ","), out oldVal);
                                    bool isNum02 = double.TryParse($"{NotifyValue}".Replace(".", ","), out newVal);
                                    // test deadband!
                                    if (!(isNum01 && isNum02))
                                    {
                                        answ = true;
                                    }
                                    else
                                    {
                                        if (Math.Abs(newVal - oldVal) >= dBand)
                                        {
                                            // indico da salvare..
                                            answ = true;
                                        }
                                    }
                                    if (answ)
                                    {
                                        lgDebug($"Test deadband reached| uuid: {uuid} | DisplayName: {dataItem.DisplayName} | oldVal: {oldVal} | newVal: {newVal} | dBand: {dBand}");
                                    }
#if DEBUG
                                    else
                                    {
                                        //lgTrace($"Test deadband NOT exceeded | uuid: {uuid} | DisplayName: {dataItem.DisplayName} | oldVal: {oldVal} | newVal: {newVal}");
                                    }
#endif
                                }
                                else
                                {
                                    answ = true;
                                }
                            }
                        }
                        if (answ)
                        {
                            // salvo!
                            dataItemMem[uuid].value = $"{NotifyValue}";
                            dataItemMem[uuid].valueTimestamp = adesso;
                            FluxMem[uuid].Value = $"{NotifyValue}";
                            FluxMem[uuid].LastUpdated = adesso;
                        }
                    }
                    else
                    {
                        // fixMet - todo: non trova word 0 allarmi....
                        // registro non trovato da aggiungere...
                        lgInfo($"DataItem non trovato in checkSaveValue: {uuid}");
                        // provo a creare oggetto in memoria...
                        try
                        {
                            int dSamplePeriod = 0;
                            float threshDBand = 0;
                            uuid = "";
                            var currDataItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, dataItem);
                            // sistemo valore/periodo
                            currDataItem.value = $"{NotifyValue}";
                            currDataItem.valueTimestamp = adesso;
                            // sistemo FluxMem
                            FluxData newFlux = new FluxData()
                            {
                                CodFlux = uuid,
                                DeadBand = threshDBand,
                                SamplePeriod = dSamplePeriod,
                                Value = $"{NotifyValue}",
                                LastUpdated = adesso
                            };
                            // aggiungo
                            dataItemMem.Add(uuid, currDataItem);
                            if (!FluxMem.ContainsKey(uuid))
                            {
                                FluxMem.Add(uuid, newFlux);
                            }
                            if (SendItemList)
                            {
                                sendDataItemListToServer(dataItem.StartNodeId);
                            }
                        }
                        catch (Exception exc)
                        {
                            lgError($"Eccezione in checkSaveSample{Environment.NewLine}{exc}");
                        }
                    }
                }
                else
                {
                    lgTrace("Attenzione: checkSaveItem con Notify null!");
                }
            }
            else
            {
                lgError("Attenzione: checkSaveItem con MonIt null!");
            }
            return answ;
        }

        /// <summary>
        /// Effettua decodifica aree memoria alla bitmap usata x MAPO
        /// </summary>
        protected virtual void decodeToBaseBitmap()
        {
            DateTime adesso = DateTime.Now;
            CheckLiveData(adesso);
            // init a zero...
            B_input = 0;

            /* -----------------------------------------------------
             *  STATE MACHINE 60 STD / SIMULA
             *------------------------------------------------------
             * bitmap MAPO
             * B0: POWER_ON
             * B1: RUN
             * B2: pzCount
             * B3: allarme
             * B4: manuale
             * B5: SlowTC (NON gestito qui)
             * B6: warm-up / cool-down
             * B7: emergenza
             ---------------------------------------------------- */

            // se valido il check ping lo eseguo... altrimenti lo do x buono
            bool checkPing = !opcUaParams.pingAsPowerOn;
            string currRun = "";
            int currTryPing = maxTryPing;
            while (currTryPing > 0 && !checkPing)
            {
                if (!checkPing)
                {
                    checkPing = (testPingMachine == IPStatus.Success);
                }
                currTryPing--;
                // se ping KO --> aspetto prima di ripetere test
                if (!checkPing)
                {
                    Random rand = new Random();
                    Thread.Sleep(rand.Next(pingServerMsTimeout * 2));
                }
            }

            // bit 0 (poweron) imposto a 1 SE pingo + PowerOn=="ON"...
            bool powerOnOk = checkPing && hasPowerOn;

            // solo se non ho veto check
            int vFactor = 2;
            if (vetoCheckStatus < adesso)
            {
                lgTrace($"Stato variabili checkPing: {testPingMachine}");
                // imposto veto per vetoSeconds...
                vetoCheckStatus = adesso.AddSeconds(vetoSeconds * vFactor);
            }

            // se abilitato watchdog...
            if (opcUaParams.WatchDog.IsEnabled)
            {
                lgTrace("WatchDog 01");
                if (adesso.Subtract(lastWatchDogPLC).TotalSeconds > 2)
                {
                    lastWatchDogPLC = adesso;
                    WatchDog++;
                    WatchDog = WatchDog > opcUaParams.WatchDog.MaxVal ? 0 : WatchDog;

                    lgTrace($"WatchDog val: {WatchDog}");
                    try
                    {
                        WriteValue commWriteVal = new WriteValue();
                        commWriteVal.NodeId = new NodeId(opcUaParams.WatchDog.MemConfWrite);
                        commWriteVal.AttributeId = Attributes.Value;
                        commWriteVal.Value = new DataValue();
                        commWriteVal.Value.Value = WatchDog;

                        List<WriteValue> nodes2Write = new List<WriteValue>();
                        nodes2Write.Add(commWriteVal);
                        UA_ref.WriteNodes(nodes2Write);
                        lgTrace("Effettuata scrittura WatchDog");
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in gestione WatchDog, valore attuale {WatchDog}{Environment.NewLine}{exc}");
                    }
                }
            }
            else
            {
                lgTrace("WatchDog disabilitato");
            }

            // log opzionale!
            if (verboseLog)
            {
                lgDebug($"Trasformazione checkPing: {checkPing} | hasPowerOn: {hasPowerOn} | B_input: {B_input} | currRun = {currRun}");
            }
        }

        /// <summary>
        /// Esegue lettura del dictionary FluxMem da REDIS + reset dataItemMem
        /// </summary>
        protected void FluxMemLoad()
        {
            // verifico se ci sia qualcosa da leggere...
            if (redisMan.redHashPresent(rKeyFluxMem))
            {
                try
                {
                    // recupero il dizionario <string,string> ...
                    Dictionary<string, string> rawDict = redisMan.redGetHashDict(rKeyFluxMem);
                    // converto!
                    FluxMem = rawDict.ToDictionary(pair => pair.Key, pair => JsonConvert.DeserializeObject<FluxData>(pair.Value));
                }
                catch (Exception exc)
                {
                    lg.Error($"Errore in FluxMemLoad{Environment.NewLine}{exc}");
                }
            }
            else
            {
                FluxMem = new Dictionary<string, FluxData>();
            }
            // infine resetto i dataItem (in attesa di capire se si possa fare a meno)
            dataItemMem = new Dictionary<string, OpcUaDataItemExt>();
        }

        /// <summary>
        /// Esegue salvataggio del dictionary dataItemMem in REDIS
        /// </summary>
        protected void FluxMemSave()
        {
            // in primis converto il dizionario serializzando i valori come string...
            try
            {
                Dictionary<string, string> keyValuePairs = FluxMem.ToDictionary(pair => pair.Key, pair => JsonConvert.SerializeObject(pair.Value));
                // salvo in redis...
                redisMan.redSaveHashDict(rKeyFluxMem, keyValuePairs);
            }
            catch (Exception exc)
            {
                lg.Error($"Errore in FluxMemSave{Environment.NewLine}{exc}");
            }
        }

        /// <summary>
        /// Recupero allarmi specifico x OPC-UA, in formato BITMAP (se è gestito a bitmap) o come
        /// INT (counter tipo BLM)
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        protected override int getAlarmStatus(BaseAlarmConf item)
        {
            int answ = 0;
            // recupero valore in formato stringa...
            string currStatus = getDataItemValue(item.memAddr);
            lgDebug($"getAlarmStatus {item.memAddr} | currStatus: {currStatus}");
            // se non nullo --> recupero int
            if (!string.IsNullOrEmpty(currStatus))
            {
                int.TryParse(currStatus, out answ);
            }
            return answ;
        }

        protected byte[] getByteRaw(DataValue obj)
        {
            if (obj == null)
                return null;

            byte[] rawByte = new byte[1];
            if (obj != null)
            {
                try
                {
                    var wrapVal = ((DataValue)obj).WrappedValue;
                    //rawByte = ObjectToByteArray(sVal);
                    var bodyVal = ((Opc.Ua.ExtensionObject)wrapVal.Value).Body;
                    rawByte = (byte[])bodyVal;
                }
                catch
                { }
            }
            return rawByte;
        }

        /// <summary>
        /// Effettua traduzione ITEM da LUT parametrica (key: tipo+id) del file di conf, se non
        /// trovo uso key
        /// </summary>
        /// <param name="tipo"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        protected override string itemTranslation(string tipo, string id)
        {
            string answ = "";
            // se c'è gestione fix nomi sistemo...
            if (opcUaParams.DictOpcNameReplace.Count > 0)
            {
                foreach (var item in opcUaParams.DictOpcNameReplace)
                {
                    id = id.Replace(item.Key, item.Value);
                }
            }
            string lemma = id;
            if (!string.IsNullOrEmpty(tipo))
            {
                lemma = $"{tipo}_{id}";
            }
            // cerco nel dizionario delle traduzioni SE esiste un valore e prendo quello, altrimenti
            // uso il lemma...
            if (opcUaParams.itemTranslation.ContainsKey(lemma))
            {
                answ = opcUaParams.itemTranslation[lemma];
            }
            else
            {
                answ = lemma;
            }
            return answ;
        }

        /// <summary>
        /// Effettua lettura file di conf specifico OPC-UA da oggetto serializzato json <paramref
        /// name="fileName">Nome file da cui leggere i parametri json</paramref>
        /// </summary>
        protected void loadOpcUaConf(string fileName)
        {
            string jsonFullPath = $"{Application.StartupPath}/DATA/CONF/{fileName}";
            lgInfo($"Apertura file {jsonFullPath}");
            using (StreamReader reader = new StreamReader(jsonFullPath))
            {
                string jsonData = reader.ReadToEnd().Replace("\n", "").Replace("\r", "");
                if (!string.IsNullOrEmpty(jsonData))
                {
                    lgDebug($"File json composto da {jsonData.Length} caratteri");
                    try
                    {
                        opcUaParams = JsonConvert.DeserializeObject<OpcUaParamConf>(jsonData);
                        lgDebug($"Decodifica aree OpcUaParamConf: trovati {opcUaParams.paramsEndThresh.Count} valori paramsEndThresh");
                        // sistemo se ci sono dati memMap...
                        memMap = new plcMemMapExt();
                        if (opcUaParams.mMapWrite != null)
                        {
                            memMap.mMapWrite = opcUaParams.mMapWrite;
                        }
                        if (opcUaParams.mMapRead != null)
                        {
                            memMap.mMapRead = opcUaParams.mMapRead;
                        }
                        setupMemMap();
                        setupOptMemPar();
                        setupFileDecod();
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in decodifica conf json OPC-UA:{Environment.NewLine}{exc}");
                    }
                }
                else
                {
                    lgError("Errore in loadOpcUaConf: file json vuoto!");
                }
            }
        }

        /// <summary>
        /// Effettua vera scrittura parametri
        /// </summary>
        /// <param name="updatedPar"></param>
        protected override void plcWriteParams(ref List<objItem> updatedPar)
        {
            // solo se connesso!
            if (connectionOk)
            {
                dataConf currMem = null;
                int byteSize = 0;
                string memAddrWrite = "";
                string serObj = "";
                if (updatedPar != null)
                {
                    // valori da scrivere in memoria
                    Dictionary<string, WriteValue> nodes2Write = new Dictionary<string, WriteValue>();
                    Dictionary<string, Dictionary<string, string>> nodesUdt2Write = new Dictionary<string, Dictionary<string, string>>();
                    Dictionary<string, string> currUdsDict = new Dictionary<string, string>();
                    // controllo i parametri... ne gestisco 4...
                    foreach (var item in updatedPar)
                    {
                        if (connectionOk)
                        {
                            Opc.Ua.DataValue currNodeVal = new DataValue();
                            try
                            {
                                memAddrWrite = "";
                                int valInt = 0;
                                double valReal = 0;
                                // cerco in area memMapWrite...
                                if (memMap.mMapWrite.ContainsKey(item.uid))
                                {
                                    // recupero!
                                    currMem = memMap.mMapWrite[item.uid];
                                    byteSize = currMem.size;
                                    memAddrWrite = currMem.memAddr;

                                    // init obj scrittura
                                    WriteValue commWriteVal = new WriteValue();
                                    commWriteVal.Value = new DataValue();
                                    commWriteVal.AttributeId = Attributes.Value;

                                    // faccio preliminarmente upsertKey...
                                    upsertKey(memAddrWrite, currMem.value);
                                    serObj = JsonConvert.SerializeObject(item);
                                    lgInfo($"Inizio processing plcWriteParams per {currMem.name} | valore richiesto {currMem.value}");
                                    lgInfo($"{Environment.NewLine}---------------UPDATED PARAM:{Environment.NewLine}{serObj}{Environment.NewLine}---------------");
                                    serObj = JsonConvert.SerializeObject(currMem);
                                    lgInfo($"{Environment.NewLine}---------------MEMORY CONTENT:{Environment.NewLine}{serObj}{Environment.NewLine}---------------");

                                    // verifico se devo processare byte[]...
                                    bool isUds = memAddrWrite.Contains(opcUaParams.kvDelim);

                                    if (isUds)
                                    {
                                        // ciclo e calcolo valore
                                        int iPos = memAddrWrite.IndexOf(opcUaParams.kvDelim);
                                        string nodeId = memAddrWrite.Substring(0, iPos);
                                        string varName = memAddrWrite.Substring(iPos + 1);

                                        // cerco se ci sia già un record Uds...
                                        if (nodesUdt2Write.ContainsKey(nodeId))
                                        {
                                            currUdsDict = nodesUdt2Write[nodeId];
                                            // controllo valore...
                                            if (currUdsDict.ContainsKey(varName))
                                            {
                                                currUdsDict[varName] = item.reqValue;
                                            }
                                            else
                                            {
                                                currUdsDict.Add(varName, item.reqValue);
                                            }
                                            nodesUdt2Write[nodeId] = currUdsDict;
                                        }
                                        else
                                        {
                                            currUdsDict = new Dictionary<string, string>();
                                            currUdsDict.Add(varName, item.reqValue);
                                            nodesUdt2Write.Add(nodeId, currUdsDict);
                                        }
                                    }
                                    else
                                    {
                                        // init generico x scrittura nell'oggetto OPC-UA direttamente
                                        commWriteVal.NodeId = new NodeId(memAddrWrite);
                                        commWriteVal.Value.Value = item.reqValue;

                                        // check tipo specifico
                                        switch (currMem.tipoMem)
                                        {
                                            case plcDataType.Boolean:
                                                break;

                                            case plcDataType.Int:
                                            case plcDataType.DInt:
                                            case plcDataType.Word:
                                            case plcDataType.DWord:
                                                int.TryParse(item.reqValue, out valInt);
                                                commWriteVal.Value.Value = valInt;
                                                break;

                                            case plcDataType.Real:
                                                double.TryParse(item.reqValue, out valReal);
                                                commWriteVal.Value.Value = valReal;
                                                break;

                                            case plcDataType.String:
                                                commWriteVal.Value.Value = item.reqValue;
                                                break;

                                            default:
                                                break;
                                        }
                                    }
                                    lgInfo($"---------------{Environment.NewLine}OPC-UA data:{Environment.NewLine}NodeId: {commWriteVal.NodeId}{Environment.NewLine}Value: {commWriteVal.Value.Value}{Environment.NewLine}---------------");

                                    // se supero soglia errori lettura --> disconnetto e resetto
                                    if (numErroriCheck >= maxErroriCheck)
                                    {
                                        lgInfo($"numErroriCheck: {numErroriCheck} --> richiesta disconnessione adapter con tryDisconnect");

                                        numErroriCheck = 0;
                                        tryDisconnect();
                                    }
                                    else
                                    {
                                        if (!string.IsNullOrEmpty(memAddrWrite) && !string.IsNullOrEmpty($"{commWriteVal.NodeId}"))
                                        {
                                            // cerco se ci fosse già --> aggiorno
                                            if (nodes2Write.ContainsKey($"{commWriteVal.NodeId}"))
                                            {
                                                nodes2Write[$"{commWriteVal.NodeId}"] = commWriteVal;
                                            }
                                            else
                                            {
                                                nodes2Write.Add($"{commWriteVal.NodeId}", commWriteVal);
                                            }
                                            // 2023.09.19 resetto richiesta
                                            item.reqValue = "";
                                        }
                                        else
                                        {
                                            lgInfo($"Errore: memAddrWrite vuoto!");
                                        }
                                    }
                                }
                                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}");
                            }
                        }
                    }

                    if (connectionOk && (nodes2Write.Count + nodesUdt2Write.Count) > 0)
                    {
                        List<WriteValue> listWriteReq = nodes2Write.Select(x => x.Value).ToList();
                        // valori "diretti"
                        if (listWriteReq.Count > 0)
                        {
                            var toWrite = new Dictionary<NodeId, IEnumerable<string>>();
                            foreach (var item in listWriteReq)
                            {
                                toWrite.Add(item.NodeId, new List<string>() { $"{item.Value.Value}" });
                            }

                            UA_ref.WriteValues(toWrite);
                            // modalità calssica alternativa?!?
#if false
                            UA_ref.WriteNodes(listWriteReq);
#endif
                        }
                        // se valori Udt con modalità alternativa
                        if (nodesUdt2Write.Count > 0)
                        {
                            foreach (var item in nodesUdt2Write)
                            {
                                UA_ref.WriteNodesUdt(item.Key, item.Value);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Per sicurezza rileggo TUTTI i valori x eventuali variazioni
        /// </summary>
        /// <returns></returns>
        protected override bool processSlowDataRead()
        {
            bool answ = false;
            DateTime adesso = DateTime.Now;
            if (enableSlowData && VetoSlowDataRead < adesso)
            {
                // metto nuovo veto
                VetoSlowDataRead = adesso.AddSeconds(IOBConfFull.FluxLog.SamplePeriod);
                int fatto = 0;
                sw.Restart();
                foreach (var item in subscribedItems)
                {
                    bool changed = false;
                    // verifico se ho i dati complessi by design
                    string currVal = "";
                    if (doByteRead)
                    {
                        try
                        {
                            // restituisce i 115 byte da deserializzare... qui HACK!
                            var rawVal = UA_ref.ReadNodeRaw(item.StartNodeId);
                            if (rawVal != null)
                            {
                                byteRawData = getByteRaw((DataValue)rawVal);
                                changed = checkAndSendRaw(item, byteRawData, true);
                                if (changed)
                                {
                                    fatto++;
                                }
                            }
                            if (readErrorCurr > 0)
                            {
                                readErrorCurr--;
                            }
                        }
                        catch (Exception exc)
                        {
                            lgError($"Eccezione durante OpcUa.processSlowDataRead per {item.StartNodeId}{Environment.NewLine}{exc}");
                            readErrorCurr++;
                        }
                    }
                    else
                    {
                        try
                        {
                            currVal = UA_ref.ReadNodeString(item.StartNodeId);
                            changed = checkAndSend(item, currVal, true);
                            if (changed)
                            {
                                fatto++;
                            }
                            if (readErrorCurr > 0)
                            {
                                readErrorCurr--;
                            }
                        }
                        catch (Exception exc)
                        {
                            lgError($"Eccezione durante OpcUa.processSlowDataRead per {item.StartNodeId}{Environment.NewLine}{exc}");
                            readErrorCurr++;
                        }
                    }
                }
                answ = subscribedItems.Count > 0;
                sw.Stop();
                lgInfo($"End processSlowDataRead | {fatto} items changed | {sw.ElapsedMilliseconds}ms");
                // controllo read error ed eventuale disconnect
                if (readErrorCurr > readErrorMax)
                {
                    lgError($"Effettuo disconnessione x superamento errori lettura UA_ref.ReadNodeString, sleetp 15 sec");
                    tryDisconnect();
                    Thread.Sleep(readErrorSleepTime);
                    readErrorCurr = 0;
                }
            }
            return answ;
        }

        protected virtual void procRunMode(ref string currRun)
        {
            // variabili RUN... se richiesto invio runMode
            if (opcUaParams.runModeSend)
            {
                bool hasVetoLK = false;
                bool hasVetoLKV = false;
                if (!string.IsNullOrEmpty(opcUaParams.keyRunMode))
                {
                    currRun = getDataItemValue(opcUaParams.keyRunMode);
                    if (!string.IsNullOrEmpty(currRun))
                    {
                        // se ho valore --> invio
                        string sVal = "";
                        string descr = $"RunModeVal";
                        DateTime locTStamp = DateTime.Now;
                        sVal = $"Change 03: {locTStamp.ToString()} | descr: {descr} | Id: {opcUaParams.keyRunMode} | Val: {currRun}";
                        // cerco se sia un dato/valore
                        hasVetoLK = opcUaParams.fluxLogKeyValVeto.ContainsKey(descr);
                        if (hasVetoLK)
                        {
                            // se c'è controllo il valore...
                            var valList = opcUaParams.fluxLogKeyValVeto[descr];
                            hasVetoLKV = valList.Contains(currRun);
                        }
                        if (hasVetoLKV)
                        {
                            lgTrace($"NON ACCODATO sample: veto trovato per {descr}/{currRun} in fluxLogKeyValVeto ", false);
                        }
                        else
                        {
                            bool sent = accodaFLog(descr, sVal, qEncodeFLog(descr, currRun));
                            if (sent)
                            {    // traccio valore DynData x analisi
                                trackDynData(descr, currRun);
                            }
                            // se richiesto provo a tradurre
                            if (opcUaParams.runModeTrad)
                            {
                                string RunModeDescr = itemTranslation("RunMode", currRun);
                                descr = $"RunMode";
                                bool sentTrad = accodaFLog(descr, sVal, qEncodeFLog(descr, RunModeDescr));
                                if (sentTrad)
                                {    // traccio valore DynData x analisi
                                    trackDynData(descr, RunModeDescr);
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Cerca la DBand specifica di un parametro
        /// </summary>
        /// <param name="keyName"></param>
        /// <returns></returns>
        protected float specDBand(string keyName)
        {
            float answ = 0;
            // cerco nel dizionario...
            if (dictActDBand.ContainsKey(keyName))
            {
                answ = dictActDBand[keyName];
            }
            // altrimenti proseguo...
            else
            {
                // cerco key in parametri end
                if (opcUaParams.paramsEndThresh.Count > 0)
                {
                    // ciclo su tutti i parametri END indicati...
                    foreach (var item in opcUaParams.paramsEndThresh)
                    {
                        if (keyName.EndsWith(item.Key))
                        {
                            answ = item.Value;
                        }
                    }
                }
                // cerco key in parametri contains
                if (opcUaParams.paramsContainsThresh.Count > 0)
                {
                    // ciclo su tutti i parametri CONTAINS indicati...
                    foreach (var item in opcUaParams.paramsContainsThresh)
                    {
                        if (keyName.Contains(item.Key))
                        {
                            answ = item.Value;
                        }
                    }
                }
                // aggiungo comunque al dizionario x velocizzare poi...
                dictActDBand.Add(keyName, answ);
            }
            return answ;
        }

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

        #endregion Protected Methods

        #region Private Fields

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

        /// <summary>
        /// ultima lettura contapezzi da server...
        /// </summary>
        private DateTime lastPzCountMesRead = DateTime.Now.AddHours(-1);

        /// <summary>
        /// Elenco degli items da monitorare come risultato del browse iniziale
        /// </summary>
        private Dictionary<string, string> selectedItemList = new Dictionary<string, string>();

        /// <summary>
        /// veto rilettura da MES del contapezzi x verifica...
        /// </summary>
        private int vetoReadPzCountMes = 1;

        /// <summary>
        /// Riferimento veto processing slowDataRead
        /// </summary>
        private DateTime VetoSlowDataRead = DateTime.Now;

        #endregion Private Fields

        #region Private Methods

        /// <summary>
        /// Conversione da valore byteArray a string del tipo obj richiesto
        /// </summary>
        /// <param name="bArray">Array byte originale</param>
        /// <param name="varDef">Stringa definizione variabile</param>
        /// <param name="pSep">Separatore nella definizione</param>
        /// <returns></returns>
        private string ConvFromBArr(byte[] bArray, string varDef, char pSep)
        {
            string answ = "";
            // converto prendendo solo parte richiesta...
            if (bArray.Length > 0)
            {
                // splitto in primis la definizione
                MemStruct currMem = new MemStruct(varDef, pSep);

                // S7.Double.B0 ora eseguo riconoscimento (da portare in base? FixMe ToDo...)
                if (currMem.Product == "S7")
                {
                    // converto!
                    plcDataType tVal = plcDataType.Int;
                    Enum.TryParse(currMem.Type, out tVal);
                    // ora gestisco seconda parte...
                    switch (tVal)
                    {
                        case plcDataType.Boolean:
                            var boolVal = S7.Net.Types.Boolean.GetValue(bArray[currMem.Idx], currMem.Num);
                            answ = $"{boolVal}";
                            break;

                        case plcDataType.Int:
                            var intVal = S7.Net.Types.Int.FromByteArray(bArray.Skip(currMem.Idx).Take(2).ToArray());
                            answ = $"{intVal}";
                            break;
                        //case plcDataType.IntLH:
                        //    break;
                        case plcDataType.DInt:
                            var dIntVal = S7.Net.Types.Int.FromByteArray(bArray.Skip(currMem.Idx).Take(4).ToArray());
                            answ = $"{dIntVal}";
                            break;
                        //case plcDataType.DIntLH:
                        //    break;
                        case plcDataType.Word:
                            var wordVal = S7.Net.Types.Word.FromByteArray((bArray.Skip(currMem.Idx).Take(2).ToArray()));
                            answ = $"{wordVal}";
                            break;

                        case plcDataType.DWord:
                            var dWordVal = S7.Net.Types.Word.FromByteArray((bArray.Skip(currMem.Idx).Take(4).ToArray()));
                            answ = $"{dWordVal}";
                            break;

                        case plcDataType.Real:
                            var dblVal = S7.Net.Types.Double.FromByteArray(bArray.Skip(currMem.Idx).Take(4).ToArray());
                            answ = $"{dblVal:N3}";
                            break;
                        //case plcDataType.RealHL:
                        //    break;
                        //case plcDataType.RealLH:
                        //    break;
                        case plcDataType.String:
                            answ = S7.Net.Types.String.FromByteArray(bArray.Skip(currMem.Idx).Take(currMem.Num).ToArray());
                            break;
                        //case plcDataType.HLPInt:
                        //    break;
                        //case plcDataType.FloatABCD:
                        //    break;
                        //case plcDataType.FloatBADC:
                        //    break;
                        //case plcDataType.FloatCDAB:
                        //    break;
                        //case plcDataType.FloatDCBA:
                        //    break;
                        //case plcDataType.BitMap:
                        //    break;
                        //case plcDataType.Byte:
                        //    break;
                        //case plcDataType.UInt:
                        //    break;
                        //case plcDataType.UDInt:
                        //    break;
                        default:
                            break;
                    }
                }
                else if (currMem.Product == "GEN")
                {
                    // ora gestisco seconda parte...
                    switch (currMem.Type)
                    {
                        case "Byte":
                            bool bVal = false;
                            if (bArray.Length > currMem.Idx)
                            {
                                bVal = bArray[currMem.Idx] == 1;
                            }
                            answ = bVal ? "1" : "0";
                            break;

                        case "Float":
                            // restituisco
                            answ = $"{BitConverter.ToSingle(bArray, currMem.Idx)}";
                            break;

                        default:
                            break;
                    }
                }
                else
                {
                    // gestire casi NON S7...
                }
            }
            return answ;
        }

        /// <summary>
        /// Vera connessione ad OpcUa
        /// </summary>
        /// <returns></returns>
        private async Task<short> doConnect()
        {
            short esitoLink = 0;
            // cerco di caricare valori precedenti FluxMem + reset dataItemMem...
            FluxMemLoad();
            // predisposizione conf oggetto di comunicazione MTC
            int port = 4840;
            int.TryParse(IOBConfFull.Device.Connect.Port, out port);
            // ora avvio
            try
            {
                lgInfo("Start init OpcUa Client");
                // Define the UA Client application
                ApplicationInstance application = new ApplicationInstance();
#if DEBUG
                application.ApplicationName = "Steamware IOB-WIN-DEB";
#else
                application.ApplicationName = "Steamware IOB-WIN Client";
#endif

                application.ApplicationType = ApplicationType.Client;

                // load della conf XML di base x l' applicazione...
                string confPath = Path.Combine(Application.StartupPath, "DATA", "IobOpcUaClient.Config.xml");
                // verifico file...
                if (!File.Exists(confPath))
                {
                    // ...provo a copiare da path Resources...
                    string srcPath = Path.Combine(Application.StartupPath, "Resources", "IobOpcUaClient.Config.xml.orig");
                    File.Copy(srcPath, confPath, true);
                }
                ////await application.LoadApplicationConfiguration(confPath, silent: false).ConfigureAwait(false);
                ////// check the application certificate.
                ////await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0).ConfigureAwait(false);
                await application.LoadApplicationConfiguration(confPath, silent: false);
                // check the application certificate.
                await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0);
                
                lgInfo($"Chiamata UAClient con configurazione standard: {application.ApplicationConfiguration.ApplicationName}");
                string userName = opcUaParams == null || opcUaParams.Identity == null ? "" : opcUaParams.Identity.UserName;
                string passwd = opcUaParams == null || opcUaParams.Identity == null ? "" : opcUaParams.Identity.Passwd;
                UA_ref = new UAClient(application.ApplicationConfiguration, IOBConfFull.General.CodIOB, userName, passwd, isVerboseLog, ClientBase.ValidateResponse, opcUaParams.maxNullRead);

                lgInfo($"Chiamata apertura OpcUa Client: {IOBConfFull.Device.Connect.IpAddr}:{port}");
                UA_ref.ServerUrl = $"opc.tcp://{IOBConfFull.Device.Connect.IpAddr}:{port}";

                var task = Task.Run(async () =>
                {
                    return await UA_ref.ConnectAsync();
                    //return await UA_ref.ConnectAsync().ConfigureAwait(false);
                });
                bool connected = task.Result;

                if (connected)
                {
                    // registro esito a positivo
                    esitoLink = 1;
                    // faccio un primo browse dei dati...
                    Dictionary<string, string> nodeIdNameList = new Dictionary<string, string>();
                    // se deve forzare lettura da subscribed parte da quelli...
                    if (opcUaParams.BrowseNodeList != null && opcUaParams.BrowseNodeList.Count > 0)
                    {
                        // effettuo browse 1:1 dei subscribed...
                        if (opcUaParams.subscribedItems != null && opcUaParams.subscribedItems.Count > 0)
                        {
                            lgInfo($"Start browse da elenco di subscribedItems: {opcUaParams.subscribedItems.Count} da esplorare");
                            Dictionary<string, string> tempList = new Dictionary<string, string>();
                            foreach (var item in opcUaParams.BrowseNodeList)
                            {
                                tempList = new Dictionary<string, string>();
                                try
                                {
                                    UA_ref.Browse(item, opcUaParams.filterItemsNodeId, ref tempList);
                                }
                                catch (Exception exc)
                                {
                                    lgError($"Eccezione in browsing subscribedItems:{Environment.NewLine}{exc}");
                                }
                                // aggiungo!
                                foreach (var foundItem in tempList)
                                {
                                    if (!nodeIdNameList.ContainsKey(foundItem.Key))
                                    {
                                        nodeIdNameList.Add(foundItem.Key, foundItem.Value);
                                    }
                                }
                            }
                        }
                    }

                    // se fosse ancora vuoto....
                    if (nodeIdNameList.Count == 0)
                    {
                        if (!string.IsNullOrEmpty(opcUaParams.BrowseFullVal))
                        {
                            lgInfo($"Start browse da BrowseFullVal: {opcUaParams.BrowseFullVal}");
                            try
                            {
                                UA_ref.Browse(opcUaParams.BrowseFullVal, opcUaParams.filterItemsNodeId, ref nodeIdNameList);
                            }
                            catch (Exception exc)
                            {
                                lgError($"Eccezione in browsing BrowseFullVal:{Environment.NewLine}{exc}");
                            }
                        }
                        else
                        {
                            lgInfo($"Start browse | BrowseNSIndex: {opcUaParams.BrowseNSIndex} | BrowseValue: {opcUaParams.BrowseValue} | filterItemsNodeId #: {opcUaParams.filterItemsNodeId.Count} | ");
                            UA_ref.Browse(opcUaParams.BrowseNSIndex, opcUaParams.BrowseValue, opcUaParams.filterItemsNodeId, ref nodeIdNameList);
                        }
                    }
                    // loggo elenco degli item sottocrivibili...
                    lgDebug($"---------- AVAILABLE FOR SUBSCRIBE : {nodeIdNameList.Count} ----------");
                    foreach (var item in nodeIdNameList)
                    {
                        lgTrace($"Avail: {item.Key}");
                    }
                    lgTrace("---------- END LIST ----------");

                    // se ho un insieme non vuoto degli item sottoscritti carico solo quelli
                    if (opcUaParams.subscribedItems != null && opcUaParams.subscribedItems.Count > 0)
                    {
                        // cerco e aggiungo SOLO quelle indicati
                        foreach (var currItem in opcUaParams.subscribedItems)
                        {
                            var foundItems = nodeIdNameList.Where(x => x.Key.Contains(currItem)).ToList();
                            if (foundItems != null && foundItems.Count > 0)
                            {
                                foreach (var fItem in foundItems)
                                {
                                    // verifico di NON duplicare...
                                    if (!selectedItemList.ContainsKey(fItem.Key))
                                    {
                                        selectedItemList.Add(fItem.Key, fItem.Value);
                                    }
                                }
                            }
                            else
                            {
                                lgDebug($"subscribedItems non trovato: {currItem}");
                            }
                        }
                        lgDebug($"Aggiunti {selectedItemList.Count} items!");
                    }
                    // altrimenti tutti!
                    else
                    {
                        selectedItemList = nodeIdNameList;
                    }

                    // loggo elenco degli item sottocrivibili...
                    lgInfo("---------- SUBSCRIBED NODES ----------");
                    foreach (var item in selectedItemList)
                    {
                        lgDebug($"SubNo: {item.Key}");
                    }
                    lgInfo($"---------- END LIST | {selectedItemList.Count} items ----------");

                    // sottoscrivo a rilevazione cambio dati solo l'incrocio degli insiemi
                    subscribedItems = UA_ref.SubscribeToDataChanges(selectedItemList);
                    // aggiungo come DataItems
                    int dSamplePeriod = 0;
                    float threshDBand = 0;
                    string uuid = "";
                    foreach (var item in subscribedItems)
                    {
                        bool changed = false;
                        OpcUaDataItemExt newItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, item);
                        FluxData newFlux = new FluxData()
                        {
                            CodFlux = uuid,
                            DeadBand = threshDBand,
                            SamplePeriod = dSamplePeriod
                        };
                        // controllo non sia già stato aggiunto
                        if (dataItemMem.ContainsKey(uuid))
                        {
                            lgDebug($"Item ALREADY subscribed: {uuid} | NOT re-adding");
                        }
                        else
                        {
                            // aggiungo dataitem
                            dataItemMem.Add(uuid, newItem);
                            lgDebug($"Item subscribed: {uuid}");
                            // verifico se ho i dati complessi by design
                            string currVal = "";
                            if (doByteRead)
                            {
                                try
                                {
                                    // restituisce i 115 byte da deserializzare... qui HACK!
                                    var rawVal = UA_ref.ReadNodeRaw(item.StartNodeId);
                                    if (rawVal != null)
                                    {
                                        byteRawData = getByteRaw((DataValue)rawVal);
                                        //lgTrace($"{uuid} | {byteRawData.Length} byte");
                                        changed = checkAndSendRaw(item, byteRawData, true);
                                        newFlux.ValueRawByte = byteRawData;
                                        // salvo dati scambiati
                                        trackExchData(byteRawData.Length);
                                    }
                                    if (readErrorCurr > 0)
                                    {
                                        readErrorCurr--;
                                    }
                                }
                                catch (Exception exc)
                                {
                                    lgError($"Eccezione durante UA_ref.ReadNodeRaw per {item.StartNodeId}{Environment.NewLine}{exc}");
                                    readErrorCurr++;
                                }
                            }
                            else
                            {
                                try
                                {
                                    currVal = UA_ref.ReadNodeString(item.StartNodeId);
                                    newFlux.Value = currVal;
                                    newItem.value = currVal;
                                    lgDebug($"Item value | {uuid}: {currVal}");
                                    // salvo comunque in dataItemMem
                                    if (dataItemMem.ContainsKey(uuid))
                                    {
                                        dataItemMem[uuid] = newItem;
                                    }

                                    changed = checkAndSend(item, currVal, IOBConfFull.FluxLog.SendOnBoot);

                                    // salvo dati scambiati
                                    trackExchData(Encoding.UTF8.GetBytes(currVal).Length);
                                    if (readErrorCurr > 0)
                                    {
                                        readErrorCurr--;
                                    }
                                }
                                catch (Exception exc)
                                {
                                    lgError($"Eccezione durante UA_ref.ReadNodeString per {item.StartNodeId}{Environment.NewLine}{exc}");
                                    readErrorCurr++;
                                }
                            }
                            // aggiungo flux!
                            if (!FluxMem.ContainsKey(uuid))
                            {
                                FluxMem.Add(uuid, newFlux);
                            }
                        }
                    }
                    // gestione ricezione null
                    UA_ref.eh_nullExceed += UA_ref_eh_nullExceed;
                    // gestione eventi change
                    UA_ref.eh_MonItChange += UA_ref_eh_MonItChange;
                    lgInfo("eh_MonItChange event registered");
                }
                // salvo in redis DataItemMem...
                FluxMemSave();

                // fix tempi!
                DateTime adesso = DateTime.Now;
                lastPzCountSend = adesso;
                lastWarnODL = adesso;
                lastCurrent = adesso;
                lgTrace($"Aggiornato doConnect.lastCurrent: {lastCurrent}");
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in doConnect{Environment.NewLine}{exc}");
            }

            // controllo read error ed eventuale disconnect
            if (readErrorCurr > readErrorMax)
            {
                lgError($"Effettuo disconnessione x superamento errori lettura UA_ref.ReadNodeString, sleep {readErrorSleepTime}msec");
                tryDisconnect();
                Thread.Sleep(readErrorSleepTime);
                readErrorCurr = 0;
            }
            return esitoLink;
        }

        /// <summary>
        /// Formatta un dataitem x salvataggio in memoria locale
        /// </summary>
        /// <param name="dSamplePeriod"></param>
        /// <param name="threshDBand"></param>
        /// <param name="uuid"></param>
        /// <param name="dataItem"></param>
        /// <returns></returns>
        private OpcUaDataItemExt formatDataItem(ref int dSamplePeriod, ref float threshDBand, ref string uuid, Opc.Ua.Client.MonitoredItem dataItem)
        {
            OpcUaDataItemExt currDataItem;
            // calcolo parametri secondo conf UniqueID
            uuid = calcID($"{dataItem.StartNodeId.Identifier}", $"{dataItem.DisplayName}");

            // SOLO SE è abilitato il datafiltering...
            if (enableDataFilter)
            {
                threshDBand = 1;
                // controllo SE ho conf x deadband...
                if (opcUaParams.paramsEndThresh.Count > 0)
                {
                    // ciclo su tutti i parametri END indicati...
                    foreach (var item in opcUaParams.paramsEndThresh)
                    {
                        if (uuid.EndsWith(item.Key))
                        {
                            threshDBand = item.Value;
                        }
                    }
                }
                if (opcUaParams.paramsContainsThresh.Count > 0)
                {
                    // ciclo su tutti i parametri CONTAINS indicati...
                    foreach (var item in opcUaParams.paramsContainsThresh)
                    {
                        if (uuid.Contains(item.Key))
                        {
                            threshDBand = item.Value;
                        }
                    }
                }
                dSamplePeriod = samplePeriodBase;
            }
            else
            {
                // provo ad alzare a 1 DBand
                //threshDBand = 0;
                threshDBand = 1;
                dSamplePeriod = samplePeriodBase;
            }
            lgDebug($"Deadband | item {uuid} | DBand {threshDBand} | samplePeriod {dSamplePeriod}");

            // salvo oggetto x "uso interno"
            currDataItem = new OpcUaDataItemExt(dataItem)
            {
                uid = uuid,
                thresholdDeadBand = threshDBand,
                samplePeriod = dSamplePeriod
            };

            return currDataItem;
        }

        /// <summary>
        /// Contapezzi assoluto macchina multi
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        private void processContapezziMulti()
        {
            /*------------------------------------------------------------
             * procedura AdHoc/semplificata per contapezzi multiplo (NO gestioni reset & co)...
             * 1) verifica il contapezzi CORRENTE x tutte le tavole da MP-IO...
             * 2) verifica valore contapezzi da conf macchina
             * 3) se ci sono variazioni invia contapezzi x la tavola corrente
             * */

            DateTime adesso = DateTime.Now;
            // rileggo da server (!!!FixMe ToDo da gestire veto lettura server ogni 1-5 minuti?
            if (adesso > lastPzCountMesRead.AddMinutes(vetoReadPzCountMes))
            {
                UpdatePzCountMes(opcUaParams.keyPartCountTav);
            }
            bool doRefresh = false;
            // ciclo sulla conf multipla contapezzi x leggere contapezzi attuali...
            foreach (var kvp in opcUaParams.keyPartCountTav)
            {
                int counterImp = 0;
                // recupero val corrente e attuale...
                var rawCount = getDataItemValue(kvp.Value);
                int.TryParse(rawCount, out counterImp);
                // verifico contapezzi corrente, se variato invio...
                if (counterImp > 0)
                {
                    // cerco se ci sia valore gestito
                    if (DictPzCountMes.ContainsKey(kvp.Key))
                    {
                        int delta = counterImp - DictPzCountMes[kvp.Key];
                        lgDebug($"Verifica Contapezzi | {kvp.Key} | Delta: {delta} | IMP: {counterImp} | MES: {DictPzCountMes[kvp.Key]}");
                        if (delta > 0)
                        {
                            string codIobTav = $"{IOBConfFull.General.CodIOB}|{kvp.Key}";
                            // invio in blocco...
                            int incrDone = trySendPzCountBlock(codIobTav, DictPzCountMes[kvp.Key], counterImp);
                            //se ha fatto incremento... registro!
                            if (incrDone > 0)
                            {
                                DictPzCountMes[kvp.Key] += incrDone;
                            }
                            // loggo
                            lgInfo($"Contapezzi MULTI inviato: {kvp.Key} | IMP: {counterImp} | MES: {DictPzCountMes[kvp.Key]}");
                            doRefresh = true;
                        }
                    }
                }
            }
            if (doRefresh)
            {
                Thread.Sleep(200);
                UpdatePzCountMes(opcUaParams.keyPartCountTav);
            }
        }

        /// <summary>
        /// Contapezzi assoluto standard
        /// </summary>
        private void processContapezziSingle()
        {
            bool hasReset = false;
            string currPzCount = "";
            // modalità 1: verifico se ho condizioni di abilitazione del contapezzi
            if (checkMultiCondition(opcUaParams.condCountEnabled) || opcUaParams.condCountEnabled.checkList.Count == 0)
            {
                lgTrace($"processContapezzi | check 01 | opcUaParams.keyPartCount: {opcUaParams.keyPartCount}");
                // cerco parametro contapezzi...
                currPzCount = getDataItemValue(opcUaParams.keyPartCount);
            }
            else
            {
                lgTrace("processContapezzi | check 01 | manca parametro per keyPartCount");
            }

            // se ho un contapezzi... processo...
            if (!string.IsNullOrEmpty(currPzCount))
            {
                int newVal = -1;
                bool fatto = Int32.TryParse(currPzCount, out newVal);

                if (fatto)
                {
                    // gestione decremento contapezzi: viene "messo via" solo SE c'è un
                    // effettivo decremento contapezzi...
                    if (newVal < contapezziPLC)
                    {
                        hasReset = true;
                        // incremento contatore richiesta
                        countKeyRichiesta = countKeyRichiesta + 1;
                        // log
                        lgInfo("Rilevato Reset Contapezzi PLC 01");
                    }

                    // salvo nuovo valore contapezziPLC
                    contapezziPLC = newVal > -1 ? newVal : contapezziPLC;
                }
                else
                {
                    lgError($"Errore in decodifica valore contapezzi, valore rilevato: {currPzCount}");
                }
            }
            else
            {
                lgError($"Errore in decodifica valore contapezzi da {opcUaParams.keyPartCount} | valore vuoto!");
            }

            // controllo comunque, se è ZERO il contapezzi, e sul server è maggiore il
            // valore x ODL e NON abilitato il trigger reset --> abilito trigger...
            if (!pzCountResetted && contapezziPLC <= IOBConfFull.Odl.ResetCountMaxPost && contapezziIOB >= IOBConfFull.Odl.ResetCountMinPre)
            {
                // controllo comunque, se è ZERO il contapezzi, e sul server è maggiore il
                // valore x ODL e NON abilitato il trigger reset --> abilito trigger...
                if (!pzCountResetted && contapezziPLC <= IOBConfFull.Odl.ResetCountMaxPost && contapezziIOB >= IOBConfFull.Odl.ResetCountMinPre)
                {
                    hasReset = true;
                    // incremento contatore richiesta
                    countKeyRichiesta = countKeyRichiesta + 1;
                    // log
                    lgInfo($"Rilevato Reset Contapezzi PLC 02 | PLC = {contapezziPLC} <= {IOBConfFull.Odl.ResetCountMaxPost} | IOB = {contapezziIOB} >= {IOBConfFull.Odl.ResetCountMinPre}) | pzCountResetted = true");
                }
                // incremento contatore richiesta
                countKeyRichiesta = countKeyRichiesta + 1;
                // log
                lgInfo($"Rilevato Reset Contapezzi PLC 02 | PLC = {contapezziPLC} <= {IOBConfFull.Odl.ResetCountMaxPost} | IOB = {contapezziIOB} >= {IOBConfFull.Odl.ResetCountMinPre}) | pzCountResetted = true");
            }

            // 2025.03.05 calcolo condizione contapezzi con NUOVO CONF
            if (IOBConfFull.Odl.ChangeOnResetCount)
            {
                pzCountResetted = hasReset;
                lgInfo("Registrato trigger pzCountResetted");
            }
            if (string.IsNullOrEmpty(opcUaParams.keyPartCount))
            {
                lgTrace("processContapezzi | check 02 | manca parametro per keyPartCount");
            }
            else
            {
                lgTrace($"processContapezzi | check 02 | opcUaParams.keyPartCount: {opcUaParams.keyPartCount}");
                // cerco parametro contapezzi... gest alternativa se è un parametro SPECIALE
                // (con "#")
                if (opcUaParams.keyPartCount.Contains(opcUaParams.kvDelim))
                {
                    int iPos = opcUaParams.keyPartCount.IndexOf(opcUaParams.kvDelim);
                    string kRef = opcUaParams.keyPartCount.Substring(0, iPos);
                    string varDef = opcUaParams.keyPartCount.Substring(iPos + 1);
                    // recupero byte
                    byte[] bArray = getDataItemValueBytes(kRef);
                    currPzCount = ConvFromBArr(bArray, varDef, opcUaParams.memDelim);
                }
                else
                {
                    currPzCount = getDataItemValue(opcUaParams.keyPartCount);
                }
            }
        }

        /// <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);
            try
            {
                utils.CallUrlPost($"{urlSaveDataItems}", rawData);
                lgInfo($"Effettuata chiamata sendDataItemsList all'url {urlSaveDataItems}");
            }
            catch (Exception exc)
            {
                lgError($"Eccezione in sendDataItemsList{Environment.NewLine} - url: {urlSaveDataItems}{Environment.NewLine}- payload:{rawData}{Environment.NewLine}Eccezione:{Environment.NewLine}{exc}");
            }
        }

        /// <summary>
        /// Ricevuto troppi null --&gt; disconnetto e traccio
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void UA_ref_eh_nullExceed(object sender, int e)
        {
            lgError($"Effettuo disconnessione x superamento letture UA_ref.ReadNodeString a NULL ({e}), sleep {2 * readErrorSleepTime}msec");
            tryDisconnect();
            Thread.Sleep(2 * readErrorSleepTime);
            readErrorCurr = 0;
        }

        /// <summary>
        /// Esegue update valori counter leggendo dal server MP-IO i valori correnti x ogni tavola
        /// </summary>
        private void UpdatePzCountMes(Dictionary<string, string> dictCounters)
        {
            // di default metto lettura 30 sec prima
            DateTime lastRead = DateTime.Now.AddSeconds(-30);
            foreach (var item in dictCounters)
            {
                int actVal = 0;
                if (CheckServerAlive())
                {
                    // recupero valore dal server MES (se disponibile)
                    string pzCountMesUrl = $"{urlGetPzCountRec}|{item.Key}";
                    string rawCount = utils.callUrl(pzCountMesUrl);
                    if (!string.IsNullOrEmpty(rawCount))
                    {
                        int.TryParse(rawCount, out actVal);
                    }
                    // se valido (>=0) altrimenti uso zero......
                    actVal = actVal >= 0 ? actVal : 0;
                    lastRead = DateTime.Now;
                }
                // se esiste aggiorno...
                if (DictPzCountMes.ContainsKey(item.Key))
                {
                    DictPzCountMes[item.Key] = actVal;
                }
                // altrimenti aggiungo
                else
                {
                    DictPzCountMes.Add(item.Key, actVal);
                }
            }
            // salvo dataora ultima lettura da MES...
            lastPzCountMesRead = lastRead;
        }

        #endregion Private Methods
    }
}