using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MP.Core.Objects; using MP.Data.DbModels; using MP.Data.Repository.IOC; using Newtonsoft.Json; using NLog; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using ZiggyCreatures.Caching.Fusion; using static MP.Core.Objects.Enums; namespace MP.Data.Services.IOC { public class IocService : BaseServ, IIocService { #region Public Constructors public IocService( IConfiguration config, IConnectionMultiplexer redis, IIocRepository repo, IServiceScopeFactory scopeFactory, //Microsoft.Extensions.Caching.Memory.IMemoryCache cache IFusionCache cache) : base(config, redis) { _className = "IocServ"; int.TryParse(config.GetValue("ServerConf:redisLongTimeCache"), out redisLongTimeCache); int.TryParse(config.GetValue("ServerConf:redisShortTimeCache"), out redisShortTimeCache); _repo = repo; _scopeFactory = scopeFactory; _cache = cache; } #endregion Public Constructors #region Public Methods /// public async Task CheckCambiaStatoBatchAsync(tipoInputEvento tipoInput, string IdxMacchina, DateTime InizioStato, int IdxTipo, string CodArt, string Value, int MatrOpr, string pallet) { bool success = await _repo.CheckCambiaStatoBatchAsync(tipoInput, IdxMacchina, InizioStato, IdxTipo, CodArt, Value, MatrOpr, pallet); return success; } /// public async Task ClearFusionCache() { bool fatto = false; await _cache.ClearAsync(); fatto = true; return fatto; } /// public async Task EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv) { bool success = await _repo.EvListMicroStatoInsertAsync(newRecMsm, newRecEv); return success; } /// public async Task GetCurrOdlAsync(string idxMacchina) { string result = ""; string currKey = $"{MP.Data.Utils.redisOdlCurrByMac}:{idxMacchina}"; // cerco in redis dato valOut sel macchina... RedisValue rawData = _redisDb.StringGet(currKey); if (rawData.HasValue) { result = $"{rawData}"; } else { result = await GetCurrOdlByProdAsync(idxMacchina); // serializzo e salvo... rawData = JsonConvert.SerializeObject(result); _redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); } return result; } /// public async Task> GetTask2ExeMacchinaAsync(string idxMacchina) { var currHash = MP.Data.Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); return await RedisGetHashDictAsync(currHash); } /// public async Task IobInsEnabAsync(string idxMacchina) { string cacheKey = $"IOC_IobInsEnab_{idxMacchina}"; return await GetOrFetchAsync(cacheKey, async () => { var key = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS); string? val = await _redisDb.HashGetAsync(key, "insEnabled"); if (val == null) { var data = await ResetDatiMacchinaAsync(idxMacchina); data.TryGetValue("insEnabled", out val); } return val != null && (val == "1" || val.ToLower() == "true"); }, TimeSpan.FromSeconds(5)); } /// public async Task MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec) { bool success = await _repo.MicroStatoMacchinaUpsertAsync(newRec); return success; } /// public async Task ProcessInputAsync(string idxMacchina, string valore, string dtEve, string dtCurr, string contatore) { string answ = ""; if (ValidateInputParams(idxMacchina, valore, dtEve, dtCurr)) { DateTime dataOraEvento = ParseEventTime(dtEve, dtCurr); // se abilitato registro evento sul DB if (await IobSLogEnabAsync(idxMacchina)) { int cntVal = 0; int.TryParse(contatore, out cntVal); await saveSigLogAsync(idxMacchina, valore, dataOraEvento, cntVal); } // continuo col resto try { // scrivo keep alive!!! (se necessario, altrimenti è in cache...) await ScriviKeepAliveAsync(idxMacchina, DateTime.Now); // Cache dati macchina per evitare lookup ridondanti Dictionary datiMacc = await mDatiMacchineAsync(idxMacchina); // verifico se sia una macchina MULTI.... if (isMulti(idxMacchina, datiMacc)) { // inizio preprocessing string newVal = ""; // processo OGNI macchina a stati dell'impianto... (KEY:IdxMacchina / IdxMacchina#qualcosa, Val = IdxFamIn) foreach (var item in await mTabMSMIAsync(idxMacchina)) { newVal = preProcInput(item.Key, valore, datiMacc); // ora processo e salvo il valOut del microstato... // INTERNAMENTE gestisce i casi DB/REDIS secondo necessità await CheckMicroStatoAsync(item.Key, newVal, dataOraEvento, contatore, datiMacc); } } else { // ora processo e salvo il valOut del microstato... INTERNAMENTE // gestisce i casi DB/REDIS secondo necessità await CheckMicroStatoAsync(idxMacchina, valore, dataOraEvento, contatore, datiMacc); } // forzo RESET dati macchina... await ResetDatiMacchinaAsync(idxMacchina); // registro in risposta che è andato tutto bene... answ = "OK"; } catch (Exception exc) { string errore = $"Errore: {Environment.NewLine}{exc}"; Log.Error(errore); answ = errore; } } return answ; } /// public async Task PzCounterTcAsync(string idxMacchina) { int answ = -1; DateTime dataRif = DateTime.Now; var datiProd = await StatoProdMacchinaAsync(idxMacchina, dataRif); if (datiProd != null) { answ = datiProd.PzTotODL; } return answ; } /// public async Task SaveCounterAsync(string idxMacchina, string counter) { string answ = "0"; // inizio processing vero e proprio INPUT... if (string.IsNullOrEmpty(idxMacchina)) { string errore = "Errore: parametro macchina vuoto"; Log.Warn(errore); answ = errore; } else { if (string.IsNullOrEmpty(counter)) { string errore = "Errore: parametro counter vuoto"; Log.Warn(errore); answ = errore; } else { int newCounter = -1; int.TryParse(counter, out newCounter); // se il conteggio è >= 0 SALVO come nuovo conteggio... if (newCounter >= 0) { var currKey = MP.Data.Utils.RedKeyPzCount(idxMacchina, MpIoNS); RedisValue rawData = await _redisDb.StringGetAsync(currKey); if (rawData.HasValue) { // salvo per + tempo... await _redisDb.StringSetAsync(currKey, answ.ToString()); answ = counter; } else { int currCount = await PzCounterTcAsync(idxMacchina); answ = currCount.ToString(); // salvo per meno tempo... await _redisDb.StringSetAsync(currKey, answ); } } } } return answ; } /// public async Task ScriviKeepAliveAsync(string IdxMacchina, DateTime oraMacchina) { // cerco se ho keep alive in redis, var currKey = MP.Data.Utils.RedKeyHash($"KeepAlive:{IdxMacchina}"); bool keyPresent = await _redisDb.KeyExistsAsync(currKey); // se NON presente salvo in REDIS con TTL 10 sec e sul DB... if (!keyPresent) { DateTime adesso = DateTime.Now; await _redisDb.StringSetAsync(currKey, adesso.ToString("s"), TimeSpan.FromSeconds(30)); // effettuo scrittura sul DB await _repo.KeepAliveUpsertAsync(IdxMacchina, DateTime.Now, oraMacchina); } } #endregion Public Methods #region Protected Fields protected Random rand = new Random(); #endregion Protected Fields #region Protected Methods /// /// Restituisce un timeout dai minuti richiesti + tempo random 1..60 sec /// /// /// protected TimeSpan getRandTOut(double stdMinutes) { double rndValue = stdMinutes + (double)rand.Next(1, 60) / 60; return TimeSpan.FromMinutes(rndValue); } #endregion Protected Methods #region Private Fields private static Logger Log = LogManager.GetCurrentClassLogger(); private readonly IFusionCache _cache; private readonly string _className; private readonly IIocRepository _repo; private readonly IServiceScopeFactory _scopeFactory; /// /// Provider CultureInfo x parse valori(es dataora) /// private CultureInfo ciProvider = CultureInfo.InvariantCulture; /// /// Formato dataora standard x parsing /// private string dtFormat = "yyyyMMddHHmmssfff"; private int redisLongTimeCache = 5; private int redisShortTimeCache = 2; #endregion Private Fields #region Private Methods /// /// controlla se da il segnale di "microstato" deriva un evento da generare - modalità OFFLINE /// /// idx macchina /// valOut ingresso /// data-ora evento (server) /// sequenza dati inviati /// dati macchina in cache (opzionale, se null fa lookup) /// private async Task CheckMicroStatoAsync(string idxMacchina, string valore, DateTime dtEve, string contatore, Dictionary? datiMaccCache = null) { // recupero SE IMPIEGATO REDIS i valori del Dictionary della macchina... Dictionary datiMacc = datiMaccCache ?? await mDatiMacchineAsync(idxMacchina); // processing inputComandoMapo answ = new inputComandoMapo(); // SE no trovassi verifico se esista la macchina altrimenti la creo... REDIS compliant if (!datiMacc.ContainsKey(idxMacchina)) { await verificaIdxMacchinaAsync(idxMacchina); } // continuo processing... string CodArticolo = datiMacc["CodArticolo"]; if (string.IsNullOrEmpty(CodArticolo)) { var allDatiMacch = await _repo.DatiMacchineGetAllAsync(); var recMacc = allDatiMacch.FirstOrDefault(x => x.IdxMacchina == idxMacchina); if (recMacc != null) { CodArticolo = recMacc.CodArticoloA; } } // preparo gestione val ingresso int? valINT = 0; int idxTipoEv = 0; int next_idxMS = 0; try { valINT = int.Parse(valore, NumberStyles.HexNumber); int idxFamIn = Convert.ToInt32(datiMacc["IdxFamIn"]); int idxMicroStato = Convert.ToInt32(datiMacc["IdxMicroStato"]); int valIOB = Convert.ToInt32(valINT); next_idxMS = idxMicroStato; // recupero singolo valOut (stringa) x chiave string todoSMI = await ValoreSmiAsync(idxFamIn, idxMicroStato, valIOB); // solo se ho trovato un risultato nella tab SMI della famiglia macchina... if (!string.IsNullOrEmpty(todoSMI) && todoSMI.Contains("_")) { // splitto e salvo valori OUT... string[] valori = todoSMI.Split('_'); idxTipoEv = Convert.ToInt32(valori[0]); next_idxMS = Convert.ToInt32(valori[1]); } } catch (Exception exc) { Log.Error($"[ChkMiSt_5a] - - Eccezione in recupero riga Trans ingressi | idxMacchina {idxMacchina} | valINT {valINT}:{Environment.NewLine}{exc}", Environment.NewLine, exc, valINT, idxMacchina); } // effettuo update vari SU DB!!! MicroStatoMacchinaModel newRecMsm = new MicroStatoMacchinaModel() { IdxMacchina = idxMacchina, IdxMicroStato = next_idxMS, InizioStato = dtEve, Value = valore }; if (idxTipoEv > 0) { // preparo record string valEsteso = string.Format("[{0}] {1}", contatore.PadLeft(3, '0'), valore); // creo evento EventListModel newRecEv = new EventListModel() { CodArticolo = CodArticolo, IdxMacchina = idxMacchina, IdxTipo = idxTipoEv, InizioStato = dtEve, MatrOpr = 0, pallet = "-", Value = valEsteso }; // salva e processa evento + microstato answ = await scriviRigaEventoAsync(newRecMsm, newRecEv); } else { await _repo.MicroStatoMacchinaUpsertAsync(newRecMsm); } return answ; } /// /// Elenco completo config da DB /// private async Task> ConfigGetAllAsync() { return await GetOrFetchAsync("IOC_ConfigAll", async () => { List? result = new List(); // cerco in redis... RedisValue rawData = await _redisDb.StringGetAsync(MP.Data.Utils.redisConfKey); string source = "DB"; if (!string.IsNullOrEmpty($"{rawData}")) { result = JsonConvert.DeserializeObject>($"{rawData}"); source = "REDIS"; } else { result = await _repo.ConfigGetAllAsync(); rawData = JsonConvert.SerializeObject(result); await _redisDb.StringSetAsync(MP.Data.Utils.redisConfKey, rawData, getRandTOut(redisLongTimeCache)); } Log.Debug($"ConfigGetAllAsync Read from {source}"); if (result == null) { result = new List(); } return result; }, TimeSpan.FromMinutes(1)); } /// /// Helper standardizzazione valOut dataora ricevuto da IOB remoti /// /// /// private string dtFormStd(string? s) { return s?.Length > 17 ? s[..17] : s?.PadRight(17, '0') ?? string.Empty; } /// /// Recupero info ODL corrente da dati prod macchina /// /// /// private async Task GetCurrOdlByProdAsync(string idxMacchina) { string answ = ""; // recupero stato... var datiProd = await StatoProdMacchinaAsync(idxMacchina, DateTime.Now); if (datiProd != null) { answ = datiProd.IdxOdl.ToString(); } // ultimo controllo su idxOdl... answ = answ == "" ? "0" : answ; // restituisco! return answ; } /// /// Implementa gestione recupero cache da memoria o da obj esterno + cache memoria /// /// /// /// /// /// private async Task GetOrFetchAsync(string cacheKey, Func> fetchFunc, TimeSpan expiration) { // GetOrSetAsync di Fusion cache: // - TryGetValue, se mancasse crea un lock solo per QUELLA key // - Esegue fetchFunc (la logica Redis + DB) // - Salva in memoria e rilascia il lock return await _cache.GetOrSetAsync( cacheKey, async ct => await fetchFunc(), options => options.SetDuration(expiration) ); } /// /// Restituisce il valOut booleano se la macchina sia abilitata all'inserimento COMPLETO nel /// Signal Log /// /// /// private async Task IobSLogEnabAsync(string idxMacchina) { bool answ = false; // ORA recupero da memoria redis... try { var currHash = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS); RedisValue rawData = await _redisDb.HashGetAsync(currHash, (RedisValue)"sLogEnabled"); // se è vuoto... leggo da DB e popolo! if (!rawData.HasValue) { await ResetDatiMacchinaAsync(idxMacchina); // riprovo rawData = await _redisDb.HashGetAsync(currHash, (RedisValue)"sLogEnabled"); } // provo conversione bool.TryParse($"{rawData}", out answ); } catch (Exception exc) { Log.Error($"Errore IobSLogEnabAsync | idxMacchina {idxMacchina}:{Environment.NewLine}{exc}"); } return answ; } /// /// Restituisce il valOut booleano se la macchina sia di tipo MULTI (con più state machine x INGRESSI) /// usando dati macchina in cache per evitare lookup ridondanti /// /// /// /// private bool isMulti(string idxMacchina, Dictionary datiMacc) { bool answ = false; try { answ = Convert.ToBoolean(datiMacc.ContainsKey("Multi") && datiMacc["Multi"] == "1"); } catch (Exception exc) { Log.Error($"Eccezione in isMulti{Environment.NewLine}{exc}"); } return answ; } private async Task> ListMasterAsync() { return await GetOrFetchAsync("IOC_ListMaster", async () => { HashSet result = new(); string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListMaster"; RedisValue rawData = await _redisDb.StringGetAsync(currKey); if (rawData.HasValue) { result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); } else { var fullList = await Macchine2SlaveGetAllAsync(); result = fullList.Select(x => x.IdxMacchina).ToHashSet(); rawData = JsonConvert.SerializeObject(result); await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); } return result; }, TimeSpan.FromMinutes(15)); } private async Task> ListSlaveAsync() { return await GetOrFetchAsync("IOC_ListSlave", async () => { HashSet result = new(); string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListSlave"; RedisValue rawData = await _redisDb.StringGetAsync(currKey); if (rawData.HasValue) { result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); } else { var fullList = await Macchine2SlaveGetAllAsync(); result = fullList.Select(x => x.IdxMacchinaSlave).Distinct().ToHashSet(); rawData = JsonConvert.SerializeObject(result); await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); } return result; }, TimeSpan.FromMinutes(15)); } /// /// Elenco completo valori Macchine 2 Slave /// /// private async Task> Macchine2SlaveGetAllAsync() { return await GetOrFetchAsync("IOC_M2SlaveGetAll", async () => { List? result = new List(); string currKey = $"{MP.Data.Utils.redisBaseAddr}:M2STab"; // cerco in redis dato valOut sel macchina... RedisValue rawData = await _redisDb.StringGetAsync(currKey); if (rawData.HasValue) { result = JsonConvert.DeserializeObject>($"{rawData}"); } else { result = await _repo.Macchine2SlaveAsync(); // serializzo e salvo... rawData = JsonConvert.SerializeObject(result); await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); } if (result == null) { result = new List(); } return result; }, TimeSpan.FromMinutes(30)); } /// /// Restitusice elenco KVP dei campi DatiMacchine + StatoMacchine per l'impianto indicato /// /// /// private async Task> mDatiMacchineAsync(string idxMacchina) { // hard coded dimensione vettore DatiMacchine Dictionary answ = new Dictionary(); // ORA recupero da memoria redis... try { var currHash = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS); answ = await RedisGetHashDictAsync(currHash); // se è vuoto... leggo da DB e popolo! if (answ.Count == 0) { answ = await ResetDatiMacchinaAsync(idxMacchina); } } catch (Exception exc) { Log.Error($"Errore in compilazione dati Macchine x Redis - idxMacchina {idxMacchina}:{Environment.NewLine}{exc}"); } return answ; } /// /// Restitusice elenco KVP (async) per evitare blocchi quando chiamato da metodi asincroni /// /// /// private async Task[]> mTabMSMIAsync(string idxMacchina) { KeyValuePair[] answ = new KeyValuePair[1]; answ[0] = new KeyValuePair("0", "0"); try { var currHash = MP.Data.Utils.RedKeyMsmi(idxMacchina); answ = await RedisGetHashAsync(currHash); if (answ == null || answ.Length == 0) { answ = await resetMSMIAsync(idxMacchina); } } catch (Exception exc) { Log.Error($"Errore in compilazione Tabella Multi State Machine Ingressi x Redis:{Environment.NewLine}{exc}"); } return answ; } /// /// Calcola dataora evento da info dt ricevute /// /// /// /// private DateTime ParseEventTime(string dtEve, string dtCurr) { DateTime dataOraEvento = DateTime.Now; // fix formato dataora in ingresso string stdEve = dtFormStd(dtEve); string stdCurr = dtFormStd(dtCurr); // 2. Se le stringhe normalizzate coincidono, skip dei calcoli if (stdEve != stdCurr) { // 3. Parsing sicuro if (DateTime.TryParseExact(stdEve, dtFormat, ciProvider, DateTimeStyles.None, out DateTime dtEvento) && DateTime.TryParseExact(stdCurr, dtFormat, ciProvider, DateTimeStyles.None, out DateTime dtCorrente)) { TimeSpan diff = dtCorrente - dtEvento; double ms = Math.Abs(diff.TotalMilliseconds); // 4. Classificazione delta con switch expression (più leggibile e performante) string deltaClass = ms switch { <= 10 => "0-10 ms", <= 100 => "10-100 ms", <= 1000 => "100-1000 ms", <= 10000 => "1-10 sec", _ => "> 10 sec" }; Log.Debug($"Correzione delta {deltaClass}"); // 5. Correzione data/ora server: sottrao lo scarto calcolato dataOraEvento = dataOraEvento.Subtract(diff); } else { Log.Error($"Errore parsing date: {stdEve} | {stdCurr}"); } } return dataOraEvento; } /// /// Calcola l'effettivo valOut da passare alla macchina a stati INGRESSI usando dati macchina in cache /// /// /// /// /// private string preProcInput(string idxMacchina, string valore, Dictionary datiMacc) { string newVal = ""; try { // variabili int valINT = 0; int BitFilt = 0; int BSR = 0; bool ExplodeBit = false; int NumBit = 0; int newValInt = 0; // recupero parametri dalla cache... int.TryParse(datiMacc.ContainsKey("BitFilt") ? datiMacc["BitFilt"] : "0", out BitFilt); int.TryParse(datiMacc.ContainsKey("BSR") ? datiMacc["BSR"] : "0", out BSR); Boolean.TryParse(datiMacc.ContainsKey("ExplodeBit") ? datiMacc["ExplodeBit"] : "false", out ExplodeBit); // non usato (x ora) int.TryParse(datiMacc.ContainsKey("NumBit") ? datiMacc["NumBit"] : "0", out NumBit); // recupero valOut valINT = int.Parse(valore, NumberStyles.HexNumber); // filtro newValInt = MP.Core.Utils.bMaskInt(valINT, BitFilt); // effettuo eventuale BitShiftRight if (BSR > 0) { newValInt = newValInt >> BSR; } // effettuo eventuale esplosione in BIT esclusivi if (ExplodeBit) { newValInt = Convert.ToInt32(1 << newValInt); } // riconverto a STRING HEX!!! newVal = newValInt.ToString("X"); } catch { newVal = valore; } return newVal; } private async Task[]> RedisGetHashAsync(RedisKey redKey) { HashEntry[] rawData = await _redisDb.HashGetAllAsync(redKey); var result = rawData.Where(x => !x.Name.IsNull).Select(x => new KeyValuePair($"{x.Name}", $"{x.Value}")).ToArray(); return result; } private async Task> RedisGetHashDictAsync(RedisKey hashKey) { HashEntry[] rawData = await _redisDb.HashGetAllAsync(hashKey); var result = rawData .Where(x => !x.Name.IsNull) .ToDictionary(x => x.Name.ToString(), x => x.Value.ToString()); return result; } private async Task RedisSetHashAsync(RedisKey redKey, KeyValuePair[] valori, double expireSeconds = -1.0) { HashEntry[] redHash = valori.Select(x => new HashEntry(x.Key, x.Value)).ToArray(); await _redisDb.HashSetAsync(redKey, redHash); if (expireSeconds > 0.0) { await _redisDb.KeyExpireAsync(redKey, DateTime.Now.AddSeconds(expireSeconds)); } } /// /// Restitusice elenco KVP dei campi DatiMacchine + StatoMacchine per l'impianto indicato /// /// /// private async Task> ResetDatiMacchinaAsync(string idxMacc) { Dictionary result = new Dictionary(); VMSFDModel? dbResult = null; dbResult = await _repo.VMSFDGetByMaccAsync(idxMacc); if (dbResult == null) return new Dictionary(); double numSecCache = redisLongTimeCache; // converto in formato dizionario... if (dbResult != null) { // salvo 1:1 i valori... STATO result.Add("IdxMicroStato", $"{dbResult.IdxMicroStato}"); result.Add("IdxStato", $"{dbResult.IdxStato}"); result.Add("CodArticolo", $"{dbResult.CodArticolo}"); result.Add("insEnabled", $"{dbResult.InsEnabled}"); result.Add("sLogEnabled", $"{dbResult.SLogEnabled}"); result.Add("pallet", $"{dbResult.Pallet}"); result.Add("CodArticolo_A", $"{dbResult.CodArticoloA}"); result.Add("CodArticolo_B", $"{dbResult.CodArticoloB}"); result.Add("TempoCicloBase", $"{dbResult.TempoCicloBase}"); result.Add("PzPalletProd", $"{dbResult.PzPalletProd}"); result.Add("MatrOpr", $"{dbResult.MatrOpr}"); result.Add("lastVal", $"{dbResult.LastVal}"); result.Add("TCBase", $"{dbResult.TempoCicloBase}"); //...e SETUP result.Add("CodMacc", $"{dbResult.Codmacchina}"); result.Add("IdxFamIn", $"{dbResult.IdxFamigliaIngresso}"); result.Add("Multi", $"{dbResult.Multi}"); result.Add("BitFilt", $"{dbResult.BitFilt}"); result.Add("MaxVal", $"{dbResult.MaxVal}"); result.Add("BSR", $"{dbResult.Bsr}"); result.Add("ExplodeBit", $"{dbResult.ExplodeBit}"); result.Add("NumBit", $"{dbResult.NumBit}"); result.Add("IdxFamMacc", $"{dbResult.IdxFamiglia}"); result.Add("simplePallet", $"{dbResult.SimplePallet}"); result.Add("palletChange", $"{dbResult.PalletChange}"); // durata cache in secondi dal valOut insEnabled... //double numSecCache = ((result["insEnabled"].ToLower() == "true") ? redisShortTimeCache : redisLongTimeCache); numSecCache = dbResult.InsEnabled ? redisShortTimeCache : redisLongTimeCache; } // dati master/slave string isMaster = (await ListMasterAsync()).Contains(idxMacc) ? "1" : "0"; string isSlave = (await ListSlaveAsync()).Contains(idxMacc) ? "1" : "0"; result.Add("Master", isMaster); result.Add("Slave", isSlave); // Processing redis in transazoine... var redKey = MP.Data.Utils.RedKeyDatiMacc(idxMacc, MpIoNS); // variazione con redis transaction... var transaction = _redisDb.CreateTransaction(); // 1. Eliminiamo la chiave (rimuove i vecchi campi) _ = transaction.KeyDeleteAsync(redKey); // 2. Prepariamo gli HashEntry per il batch (più efficiente di un Dictionary) HashEntry[] entries = result.Select(kvp => new HashEntry(kvp.Key, kvp.Value)).ToArray(); // 3. Inseriamo i nuovi valori _ = transaction.HashSetAsync(redKey, entries); // 4. Impostiamo l'expiration in un unico colpo _ = transaction.KeyExpireAsync(redKey, TimeSpan.FromSeconds(numSecCache)); // Eseguiamo tutto in un unico viaggio verso Redis bool success = await transaction.ExecuteAsync(); return result; } /// /// Resetta (rileggendo) i dati della State Machine multi ingressi nel formato /// currKey: IdxMacchina /// value: IdxFamigliaIngresso /// /// /// private async Task[]> resetMSMIAsync(string idxMacchina) { var currHash = MP.Data.Utils.RedKeyMsmi(idxMacchina); // recupero records var tabMSMI = await _repo.VMSFDGetMultiByMaccAsync(idxMacchina); KeyValuePair[] answ = new KeyValuePair[tabMSMI.Count]; // salvo tutti i valori StateMachineIngressi... int i = 0; foreach (var item in tabMSMI) { answ[i] = new KeyValuePair(item.IdxMacchina, item.IdxFamigliaIngresso.ToString()); i++; } // verifico il timeout (default 60 sec...) var sTOutSmi = await tryGetConfigAsync("TmOut.MSMI"); int tOut = 60; int.TryParse(sTOutSmi, out tOut); tOut = tOut <= 60 ? 60 : tOut; // salvo in redis! await RedisSetHashAsync(currHash, answ, tOut); return answ; } /// /// Resetta (rileggendo) i dati della State Machine ingressi nel formato /// currKey: cState_nVal (current MICRO-STATE + "_" + new Value) /// value: iTipoEv_nState (IdxTipoEv da trasmettere + New MICRO-STATE) /// /// /// private async Task[]> resetSMIAsync(int idxFamIn) { var currHash = MP.Data.Utils.GetHashSMI(idxFamIn); // leggo da DB... var tabSMI = await _repo.StateMachineIngressiAsync(idxFamIn); KeyValuePair[] answ = new KeyValuePair[tabSMI.Count]; // salvo tutti i valori StateMachineIngressi... int i = 0; string key = ""; string val = ""; foreach (var item in tabSMI) { key = string.Format("{0}_{1}", item.IdxMicroStato, item.ValoreIngresso); val = string.Format("{0}_{1}", item.IdxTipoEvento, item.NextIdxMicroStato); answ[i] = new KeyValuePair(key, val); i++; } // verifico il timeout (default 60 sec...) int tOut = 300; var sTOutSmi = await tryGetConfigAsync("TmOut.SMI"); if (!string.IsNullOrEmpty(sTOutSmi)) { int.TryParse(sTOutSmi, out tOut); } // salvo in redis! await RedisSetHashAsync(currHash, answ, tOut); return answ; } /// /// salva il segnale di "microstato" (segnale) ASYNC /// /// idx macchina /// valOut ingresso /// data-ora evento (server) /// contatore sequenza dati inviati /// private async Task saveSigLogAsync(string idxMacchina, string valore, DateTime dtEve, int contatore) { SignalLogModel newRec = new SignalLogModel() { IdxMacchina = idxMacchina, DtCurr = DateTime.Now, DtEve = dtEve, Contatore = contatore, Valore = valore }; return await _repo.SignalLogInsertAsync(newRec); } /// /// Scrive una riga evento + una riga microstato insieme, ed effettua verifica necessità cambio stato /// /// Record MicroStatoMacchina /// record EventList /// private async Task scriviRigaEventoAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv) { bool inserito = false; // inserisco evento inserito = await _repo.EvListMicroStatoInsertAsync(newRecMsm, newRecEv); // faccio controllo per eventuale cambio stato da tab transizioni... await _repo.CheckCambiaStatoBatchAsync(tipoInputEvento.hw, newRecEv.IdxMacchina, newRecEv.InizioStato ?? DateTime.Now, newRecEv.IdxTipo, newRecEv.CodArticolo, newRecEv.Value, newRecEv.MatrOpr, newRecEv.pallet); // formatto output inputComandoMapo answ = new inputComandoMapo(); answ.outValue = inserito.ToString(); answ.needStatusRefresh = true; return answ; } /// /// Restitusice elenco KVP dei campi della State Machine ingressi nel formato /// currKey: cState_nVal (current MICRO-STATE + "_" + new Value) /// value: iTipoEv_nState (IdxTipoEv da trasmettere + New MICRO-STATE /// /// /// private async Task[]> StateMachInByKeyAsync(int idxFamIn) { // hard coded dimensione vettore DatiMacchine KeyValuePair[] answ = new KeyValuePair[1]; // iniziualizzo con un valOut... 0/0 answ[0] = new KeyValuePair("0", "0"); // ORA recupero da memoria redis... try { var currHash = MP.Data.Utils.GetHashSMI(idxFamIn); answ = await RedisGetHashAsync(currHash); // se è vuoto... leggo da DB e popolo! if (answ.Length == 0) { answ = await resetSMIAsync(idxFamIn); } } catch (Exception exc) { Log.Error($"Errore in compilazione State Machine Ingressi x Redis:{Environment.NewLine}{exc}"); } return answ; } /// /// Stato prod macchina (completo) /// /// /// /// private async Task StatoProdMacchinaAsync(string idxMacchina, DateTime dtReq, bool forceDb = false) { string cacheKey = $"IOC_StatoProd_{idxMacchina}"; return await GetOrFetchAsync(cacheKey, async () => { StatoProdModel? result = new StatoProdModel(); // cerco in _redisConn... string currKey = $"{MP.Data.Utils.redisStatoProd}:{idxMacchina}:{dtReq:HHmm}"; RedisValue rawData = await _redisDb.StringGetAsync(currKey); if (rawData.HasValue && !forceDb) { result = JsonConvert.DeserializeObject($"{rawData}"); } else { result = await _repo.StatoProdMacchinaAsync(idxMacchina, dtReq); // serializzo e salvo... rawData = JsonConvert.SerializeObject(result); await _redisDb.StringSetAsync(currKey, rawData, TimeSpan.FromSeconds(30)); } if (result == null) { result = new StatoProdModel(); } return result; }, TimeSpan.FromSeconds(3)); } /// /// Restituisce valOut della stringa (SE disponibile) /// /// /// private async Task tryGetConfigAsync(string keyName) { string answ = ""; // preselezione valori var configData = await ConfigGetAllAsync(); var currRec = configData.FirstOrDefault(x => x.Chiave == keyName); if (currRec != null) { answ = currRec.Valore; } return answ; } /// /// Validazione preliminare valori input /// /// /// /// /// /// private bool ValidateInputParams(string idxMacchina, string valore, string dtEve, string dtCurr) { bool isValid = false; // preparo stringa valori correnti string currVals = $"idxMacchina: {idxMacchina} | valOut: {valore} | dtEve: {dtEve} | dtCurr:{dtCurr}"; if (dtEve == null || dtCurr == null) { Log.Warn($"procInput: null found | {currVals}"); } else if (dtEve.Length < 17 || dtCurr.Length < 17) { Log.Info($"procInput: invalid data | {currVals}"); } else if (string.IsNullOrEmpty(idxMacchina)) { Log.Info($"procInput: missing IdxMacchina | {currVals}"); } else if (string.IsNullOrEmpty(valore)) { Log.Info($"procInput: missing valOut | {currVals}"); } else { isValid = true; } return isValid; } /// /// Restituisce il valore SPECIFICATO per la state machine ingressi /// value: iTipoEv_nState (IdxTipoEv da trasmettere + New MICRO-STATE) /// /// /// /// /// private async Task ValoreSmiAsync(int idxFamIn, int idxMicroStato, int valoreIn) { string valOut = ""; var currHash = MP.Data.Utils.GetHashSMI(idxFamIn); string field = $"{idxMicroStato}_{valoreIn}"; var searchVal = await _redisDb.HashGetAsync(currHash, field); if (!searchVal.HasValue) { // ricarico tabella (salvando in redis x ricerca successiva)! var valori = await StateMachInByKeyAsync(idxFamIn); if (valori.Any(x => x.Key == field)) { searchVal = valori.FirstOrDefault(x => x.Key == field).Value; } } valOut = $"{searchVal}"; return valOut; } /// /// cerca codice in anagrafica macchine ed eventualmente inserisce nuova macchina /// /// private async Task verificaIdxMacchinaAsync(string IdxMacchina) { // esecuzione in REDIS...cerco status macchina... if ((await mDatiMacchineAsync(IdxMacchina)).Count == 0) { // verifico se esiste su DB var dbRec = await _repo.MacchineGetByIdxAsync(IdxMacchina); if (dbRec == null) { MacchineModel newRec = new MacchineModel() { IdxMacchina = IdxMacchina, CodMacchina = "0000", Nome = IdxMacchina, Descrizione = "Macchina non codificata", Note = "-", locazione = "", RecipeArchivePath = "", RecipePath = "" }; await _repo.MacchineUpsertAsync(newRec); // verifico ci sia un microstato macchina... var recMSM = await _repo.MicroStatoMacchinaGetByIdxMaccAsync(IdxMacchina); if (recMSM.Count == 0) { // inserisco nuovo stato... MicroStatoMacchinaModel msRec = new MicroStatoMacchinaModel() { IdxMacchina = IdxMacchina, IdxMicroStato = 0, InizioStato = DateTime.Now, Value = "00" }; await _repo.MicroStatoMacchinaUpsertAsync(msRec); } } } } #endregion Private Methods } }