From 523bf78d33df6febd0dd53893fc2cd7b3c5fdb37 Mon Sep 17 00:00:00 2001 From: Samuele Locatelli Date: Thu, 30 Apr 2026 17:58:55 +0200 Subject: [PATCH] Update pesante x gestione servizio x accesso IOC repo --- MP.Data/Repository/IOC/IIocRepository.cs | 75 ++ MP.Data/Repository/IOC/IocRepository.cs | 171 ++++- MP.Data/Services/IOC/IIocService.cs | 11 + MP.Data/Services/IOC/IocService.cs | 910 ++++++++++++++++++++++- MP.IOC/Controllers/IOBController.cs | 28 +- MP.IOC/MP.IOC.csproj | 2 +- MP.IOC/Resources/ChangeLog.html | 2 +- MP.IOC/Resources/VersNum.txt | 2 +- MP.IOC/Resources/manifest.xml | 2 +- 9 files changed, 1184 insertions(+), 19 deletions(-) diff --git a/MP.Data/Repository/IOC/IIocRepository.cs b/MP.Data/Repository/IOC/IIocRepository.cs index 60537ec1..cfa5824f 100644 --- a/MP.Data/Repository/IOC/IIocRepository.cs +++ b/MP.Data/Repository/IOC/IIocRepository.cs @@ -1,5 +1,6 @@ using MP.Data.DbModels; using System; +using System.Collections.Generic; using System.Threading.Tasks; using static MP.Core.Objects.Enums; @@ -26,6 +27,18 @@ namespace MP.Data.Repository.IOC /// Task CheckCambiaStatoBatchAsync(tipoInputEvento tipoInput, string IdxMacchina, DateTime InizioStato, int IdxTipo, string CodArt, string Value, int MatrOpr, string pallet); + /// + /// Elenco da tabella Config + /// + /// + Task> ConfigGetAllAsync(); + + /// + /// Intera tab dati macchina + /// + /// + Task> DatiMacchineGetAllAsync(); + /// /// Aggiunta record MicroStato + EventList /// @@ -34,6 +47,41 @@ namespace MP.Data.Repository.IOC /// Task EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv); + /// + /// Upsert record keepalive + /// + /// + /// + /// + /// + Task KeepAliveUpsertAsync(string IdxMacc, DateTime OraServer, DateTime OraMacc); + + /// + /// Intera tabella relazione master/slave in machine (gestione setup master - slave) + /// + /// + Task> Macchine2SlaveAsync(); + + /// + /// Recupera record macchina da Idx + /// + /// + /// + Task MacchineGetByIdxAsync(string IdxMacchina); + + /// + /// Upsert Record Macchine ASYNC + /// + /// + Task MacchineUpsertAsync(MacchineModel entity); + + /// + /// Elenco da tabella Macchine + /// + /// + /// + Task> MicroStatoMacchinaGetByIdxMaccAsync(string IdxMacc); + /// /// Aggiornamento record Microstato macchina /// @@ -41,6 +89,33 @@ namespace MP.Data.Repository.IOC /// Task MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec); + /// + /// Aggiunta record SignalLog Async + /// + /// + /// + Task SignalLogInsertAsync(SignalLogModel newRec); + + /// + /// Intera tabella state machine ingressi 2 eventi + /// + /// + Task> StateMachineIngressiAsync(int idxFam); + + /// + /// Vista v_MSFD x singola macchina (da stored) - singolo record + /// + /// + /// + Task VMSFDGetByMaccAsync(string idxMacc); + + /// + /// Vista v_MSFD delle machine MULTI filtrato x macchina (da stored) + /// + /// + /// + Task> VMSFDGetMultiByMaccAsync(string idxMacc); + #endregion Public Methods } } \ No newline at end of file diff --git a/MP.Data/Repository/IOC/IocRepository.cs b/MP.Data/Repository/IOC/IocRepository.cs index d01011c3..de11638e 100644 --- a/MP.Data/Repository/IOC/IocRepository.cs +++ b/MP.Data/Repository/IOC/IocRepository.cs @@ -1,8 +1,10 @@ using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; using MP.Data.DbModels; using NLog; using System; +using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; @@ -99,6 +101,30 @@ namespace MP.Data.Repository.IOC } } + /// + public async Task> ConfigGetAllAsync() + { + await using var dbCtx = await CreateContextAsync(); + var dbResult = await dbCtx + .DbSetConfig + .AsNoTracking() + .OrderBy(x => x.Chiave) + .ToListAsync(); + return dbResult; + } + + /// + public async Task> DatiMacchineGetAllAsync() + { + await using var dbCtx = await CreateContextAsync(); + var dbResult = await dbCtx + .DbSetDatiMacchine + .AsNoTracking() + .OrderBy(x => x.IdxMacchina) + .ToListAsync(); + return dbResult; + } + /// public async Task EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv) { @@ -145,10 +171,103 @@ namespace MP.Data.Repository.IOC } } + /// + public async Task KeepAliveUpsertAsync(string IdxMacc, DateTime OraServer, DateTime OraMacc) + { + await using var dbCtx = await CreateContextAsync(); + bool fatto = false; + + var currRec = await dbCtx + .DbSetKeepAlive + .Where(x => x.IdxMacchina == IdxMacc) + .FirstOrDefaultAsync(); + if (currRec != null) + { + currRec.DataOraServer = OraServer; + currRec.DataOraMacchina = OraMacc; + dbCtx.Entry(currRec).State = EntityState.Modified; + } + else + { + KeepAliveModel newRec = new KeepAliveModel() + { + IdxMacchina = IdxMacc, + DataOraMacchina = OraMacc, + DataOraServer = OraServer, + DataOraStart = DateTime.Now + }; + dbCtx + .DbSetKeepAlive + .Add(newRec); + } + fatto = await dbCtx.SaveChangesAsync() > 0; + + return fatto; + } + + /// + public async Task> Macchine2SlaveAsync() + { + await using var dbCtx = await CreateContextAsync(); + List dbResult = await dbCtx + .DbSetM2S + .AsNoTracking() + .OrderBy(x => x.IdxMacchina) + .ToListAsync(); + + return dbResult; + } + + /// + public async Task MacchineGetByIdxAsync(string IdxMacchina) + { + await using var dbCtx = await CreateContextAsync(); + MacchineModel dbResult = await dbCtx + .DbSetMacchine + .FirstOrDefaultAsync(x => x.IdxMacchina == IdxMacchina); + return dbResult; + } + + /// + public async Task MacchineUpsertAsync(MacchineModel entity) + { + await using var dbCtx = await CreateContextAsync(); + + // Recuperiamo l'entità tracciata dal context + var trackedEntity = await dbCtx + .DbSetMacchine + .FirstOrDefaultAsync(x => x.IdxMacchina == entity.IdxMacchina); + + if (trackedEntity != null) + { + // Aggiorna i valori dell'entità tracciata con quelli della nuova + dbCtx.Entry(trackedEntity).CurrentValues.SetValues(entity); + } + else + { + dbCtx.DbSetMacchine.Update(entity); + } + return await dbCtx.SaveChangesAsync() > 0; + } + + /// + public async Task> MicroStatoMacchinaGetByIdxMaccAsync(string IdxMacc) + { + await using var dbCtx = await CreateContextAsync(); + List dbResult = new List(); + dbResult = await dbCtx + .DbSetMicroStatoMacc + .Where(x => x.IdxMacchina == IdxMacc) + .AsNoTracking() + .ToListAsync(); + return dbResult; + } + + /// public async Task MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec) { - bool fatto = false; await using var dbCtx = await CreateContextAsync(); + bool fatto = false; var actRec = await dbCtx .DbSetMicroStatoMacc @@ -174,6 +293,56 @@ namespace MP.Data.Repository.IOC return fatto; } + /// + public async Task SignalLogInsertAsync(SignalLogModel newRec) + { + await using var dbCtx = await CreateContextAsync(); + var currRec = dbCtx + .DbSetSignalLog + .Add(newRec); + return await dbCtx.SaveChangesAsync() > 0; + } + + /// + public async Task> StateMachineIngressiAsync(int idxFam) + { + await using var dbCtx = await CreateContextAsync(); + var IdxFamIn = new SqlParameter("@IdxFamigliaIngresso", idxFam); + var dbResult = await dbCtx + .DbSetSMI + .FromSqlRaw("exec dbo.stp_TRI_getByIdxFamIng @IdxFamigliaIngresso", IdxFamIn) + .AsNoTracking() + .ToListAsync(); + return dbResult; + } + + /// + public async Task VMSFDGetByMaccAsync(string idxMacc) + { + await using var dbCtx = await CreateContextAsync(); + var IdxMacchina = new SqlParameter("@IdxMacchina", idxMacc); + var dbResult = (await dbCtx + .DbSetMSFD + .FromSqlRaw("exec dbo.stp_MSFD_getMacc @IdxMacchina", IdxMacchina) + .AsNoTracking() + .ToListAsync()) + .FirstOrDefault(); + return dbResult; + } + + /// + public async Task> VMSFDGetMultiByMaccAsync(string idxMacc) + { + await using var dbCtx = await CreateContextAsync(); + var IdxMacchina = new SqlParameter("@IdxMacchina", idxMacc); + var dbResult = await dbCtx + .DbSetMSFD + .FromSqlRaw("exec dbo.stp_MSFD_getMulti @IdxMacchina", IdxMacchina) + .AsNoTracking() + .ToListAsync(); + return dbResult; + } + #endregion Public Methods #region Protected Fields diff --git a/MP.Data/Services/IOC/IIocService.cs b/MP.Data/Services/IOC/IIocService.cs index d723e812..f1376610 100644 --- a/MP.Data/Services/IOC/IIocService.cs +++ b/MP.Data/Services/IOC/IIocService.cs @@ -38,6 +38,17 @@ namespace MP.Data.Services.IOC /// Task MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec); + /// + /// Processa input da IOB eventualmente registrando i segnali inviati + /// + /// + /// + /// + /// + /// + /// + Task ProcessInputAsync(string idxMacchina, string valore, string dtEve, string dtCurr, string contatore); + #endregion Public Methods } } \ No newline at end of file diff --git a/MP.Data/Services/IOC/IocService.cs b/MP.Data/Services/IOC/IocService.cs index bda957c9..cce02490 100644 --- a/MP.Data/Services/IOC/IocService.cs +++ b/MP.Data/Services/IOC/IocService.cs @@ -1,8 +1,15 @@ 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 static MP.Core.Objects.Enums; @@ -15,10 +22,14 @@ namespace MP.Data.Services.IOC public IocService( IConfiguration config, IConnectionMultiplexer redis, - IIocRepository repo) : base(config, redis) + IIocRepository repo, + IServiceScopeFactory scopeFactory) : 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; } #endregion Public Constructors @@ -32,6 +43,37 @@ namespace MP.Data.Services.IOC return success; } + /// + /// Elenco completo config da DB + /// + /// + public async Task> ConfigGetAllAsync() + { + 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(); + //result = await Task.FromResult(SpecDbController.ConfigGetAllAsync()); + // serializzo e salvo... + 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; + } + /// public async Task EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv) { @@ -39,6 +81,66 @@ namespace MP.Data.Services.IOC return success; } + /// + /// Restituisce il valOut booleano se la macchina sia abilitata all'inserimento COMPLETO nel + /// Signal Log + /// + /// + /// + public 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; + } + + /// + /// Elenco completo valori Macchine 2 Slave + /// + /// + public async Task> Macchine2SlaveGetAllAsync() + { + 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; + } + /// public async Task MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec) { @@ -46,13 +148,819 @@ namespace MP.Data.Services.IOC 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; + } + + /// + /// scrive un evento di keepalive sulla tabella + /// + /// + /// + /// + 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 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; + } + + /// + /// 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; + } + + /// + /// 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() + { + 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(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); + } + return result; + } + + private async Task> ListSlaveAsync() + { + 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(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); + } + return result; + } + + /// + /// 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; + } + else + { + } + // 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; + } + + /// + /// 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 } } \ No newline at end of file diff --git a/MP.IOC/Controllers/IOBController.cs b/MP.IOC/Controllers/IOBController.cs index 21ef2c34..563f5cca 100644 --- a/MP.IOC/Controllers/IOBController.cs +++ b/MP.IOC/Controllers/IOBController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using MP.Core.DTO; using MP.Core.Objects; using MP.Data.DbModels; +using MP.Data.Services.IOC; using MP.IOC.Data; using Newtonsoft.Json; using NLog; @@ -16,17 +17,18 @@ namespace MP.IOC.Controllers { #region Public Constructors - public IOBController(IConfiguration configuration, MpDataService DataService) + public IOBController(IConfiguration configuration, MpDataService DataService, IIocService IService) { _configuration = configuration; DService = DataService; + IOCService = IService; } #endregion Public Constructors #region Public Methods - /// + /// /// SALVA x macchina KVP parametro/valore: /// GET: api/IOB/addOptPar/SIMUL_03?pName=PZREQ&pValue=1000 /// @@ -327,15 +329,6 @@ namespace MP.IOC.Controllers } } - /// - /// Chiude ODL precedente ed avvia uno nuovo (duplicandolo e sitemando quantità RIMANENTE), - /// e CONFERMA produzione... - /// - /// GET: IOB/forceSplitOdl/SIMUL_03 - /// - /// - /// Esito chiamata (OK/vuoto) - [HttpGet("forceSplitOdl/{id}")] public async Task ForceSplitOdl(string id) { @@ -358,6 +351,14 @@ namespace MP.IOC.Controllers return Ok(answ); } + /// + /// Chiude ODL precedente ed avvia uno nuovo (duplicandolo e sitemando quantità RIMANENTE), + /// e CONFERMA produzione... + /// + /// GET: IOB/forceSplitOdl/SIMUL_03 + /// + /// + /// Esito chiamata (OK/vuoto) /// /// Recupera ArtNum dato CodXdl (per impianti che accettano solo INT in scrittura): /// GET: IOB/getArtNum/SIMUL_03?CodXdl=ABC123 @@ -880,7 +881,8 @@ namespace MP.IOC.Controllers DateTime dataOraEvento = DateTime.Now; try { - answ = await DService.ProcessInputAsync(id, valore, dtEve, dtCurr, cnt); + answ = await IOCService.ProcessInputAsync(id, valore, dtEve, dtCurr, cnt); + //answ = await DService.ProcessInputAsync(id, valore, dtEve, dtCurr, cnt); return Ok(answ); } catch (Exception exc) @@ -1447,8 +1449,8 @@ namespace MP.IOC.Controllers #region Private Fields private static IConfiguration _configuration = null!; - private static Logger Log = LogManager.GetCurrentClassLogger(); + private IIocService IOCService; #endregion Private Fields diff --git a/MP.IOC/MP.IOC.csproj b/MP.IOC/MP.IOC.csproj index f5d79982..0d8916eb 100644 --- a/MP.IOC/MP.IOC.csproj +++ b/MP.IOC/MP.IOC.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 8.16.2604.3016 + 8.16.2604.3017 diff --git a/MP.IOC/Resources/ChangeLog.html b/MP.IOC/Resources/ChangeLog.html index 8babd7b3..d84fdcbc 100644 --- a/MP.IOC/Resources/ChangeLog.html +++ b/MP.IOC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MP-IOC -

Versione: 8.16.2604.3016

+

Versione: 8.16.2604.3017


Note di rilascio:
  • diff --git a/MP.IOC/Resources/VersNum.txt b/MP.IOC/Resources/VersNum.txt index ba173677..291a80f5 100644 --- a/MP.IOC/Resources/VersNum.txt +++ b/MP.IOC/Resources/VersNum.txt @@ -1 +1 @@ -8.16.2604.3016 +8.16.2604.3017 diff --git a/MP.IOC/Resources/manifest.xml b/MP.IOC/Resources/manifest.xml index ced1d562..e80112ee 100644 --- a/MP.IOC/Resources/manifest.xml +++ b/MP.IOC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 8.16.2604.3016 + 8.16.2604.3017 https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html false