From f95e7c441b58d7ba2d934a559e72210c72cb2629 Mon Sep 17 00:00:00 2001 From: Samuele Locatelli Date: Fri, 8 May 2026 08:03:23 +0200 Subject: [PATCH] Aggiunta progetto API Routing semplificata (solo routing) --- Directory.Packages.props | 4 +- MP-RIOC/Data/MpDataService.cs | 5006 +++++++++++++++++++ MP-RIOC/MP-RIOC.http | 6 + MP-RIOC/MP-RIOC.slnx | 4 + MP-RIOC/MP.RIOC.csproj | 41 + MP-RIOC/Pages/Index.cshtml | 25 + MP-RIOC/Pages/Index.cshtml.cs | 12 + MP-RIOC/Program.cs | 131 + MP-RIOC/Properties/launchSettings.json | 41 + MP-RIOC/Services/IWeightProvider.cs | 29 + MP-RIOC/Services/InMemoryWeightProvider.cs | 56 + MP-RIOC/Services/PreserveBodyTransformer.cs | 38 + MP-RIOC/Services/RedisWeightProvider.cs | 157 + MP-RIOC/Services/RouteManager.cs | 167 + MP-RIOC/Services/RouteStatsManager.cs | 71 + MP-RIOC/WeatherForecast.cs | 13 + MP-RIOC/appsettings.Development.json | 8 + MP-RIOC/appsettings.json | 79 + MP-RIOC/compilerconfig.json | 6 + MP-RIOC/compilerconfig.json.defaults | 59 + MP-RIOC/post-build.ps1 | 32 + MP.IOC/Program.cs | 13 +- MP.IOC/appsettings.json | 9 + 23 files changed, 6004 insertions(+), 3 deletions(-) create mode 100644 MP-RIOC/Data/MpDataService.cs create mode 100644 MP-RIOC/MP-RIOC.http create mode 100644 MP-RIOC/MP-RIOC.slnx create mode 100644 MP-RIOC/MP.RIOC.csproj create mode 100644 MP-RIOC/Pages/Index.cshtml create mode 100644 MP-RIOC/Pages/Index.cshtml.cs create mode 100644 MP-RIOC/Program.cs create mode 100644 MP-RIOC/Properties/launchSettings.json create mode 100644 MP-RIOC/Services/IWeightProvider.cs create mode 100644 MP-RIOC/Services/InMemoryWeightProvider.cs create mode 100644 MP-RIOC/Services/PreserveBodyTransformer.cs create mode 100644 MP-RIOC/Services/RedisWeightProvider.cs create mode 100644 MP-RIOC/Services/RouteManager.cs create mode 100644 MP-RIOC/Services/RouteStatsManager.cs create mode 100644 MP-RIOC/WeatherForecast.cs create mode 100644 MP-RIOC/appsettings.Development.json create mode 100644 MP-RIOC/appsettings.json create mode 100644 MP-RIOC/compilerconfig.json create mode 100644 MP-RIOC/compilerconfig.json.defaults create mode 100644 MP-RIOC/post-build.ps1 diff --git a/Directory.Packages.props b/Directory.Packages.props index de7d50f2..00d6ceed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,9 +28,9 @@ - + - + diff --git a/MP-RIOC/Data/MpDataService.cs b/MP-RIOC/Data/MpDataService.cs new file mode 100644 index 00000000..12eecbd8 --- /dev/null +++ b/MP-RIOC/Data/MpDataService.cs @@ -0,0 +1,5006 @@ +using Microsoft.EntityFrameworkCore; +using MP.Core.Conf; +using MP.Core.DTO; +using MP.Core.Objects; +using MP.Data; +using MP.Data.Controllers; +using MP.Data.DbModels; +using MP.Data.DbModels.Anag; +using MP.Data.MgModels; +using MP.Data.Services.IOC; +using MP.Data.Services.Mtc; +using Newtonsoft.Json; +using NLog; +using StackExchange.Redis; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using static MP.Core.Objects.Enums; + +namespace MP.RIOC.Data +{ + public class MpDataService : IDisposable + { + #region Public Constructors + + public MpDataService( + IConfiguration configuration, + ILogger logger, + IServiceScopeFactory scopeFactory, + IMtcSetupService mtcServ) + { + _logger = logger; + _logger.LogInformation("Starting MpDataService INIT"); + _configuration = configuration; + _scopeFactory = scopeFactory; + + // setup compoenti REDIS + redisConn = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("Redis")); + redisConnAdmin = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("RedisAdmin")); + redisDb = redisConn.GetDatabase(); + BroadastMsgPipe = new MessagePipe(redisConn, Constants.BROADCAST_M_PIPE); + // leggo cache (lungo periodo e corto periodo) + int.TryParse(_configuration.GetValue("ServerConf:redisLongTimeCache"), out redisLongTimeCache); + int.TryParse(_configuration.GetValue("ServerConf:redisShortTimeCache"), out redisShortTimeCache); + Log.Info($"Redis INIT | redisLongTimeCache: {redisLongTimeCache} min | redisShortTimeCache {redisShortTimeCache} min"); + + // gestione scope factory + useFactory = _configuration.GetValue("ServerConf:useFactory"); + // conf base x servizi condivisi IO/IOC + MpIoNS = _configuration.GetValue("ServerConf:MpIoNS") ?? "MP"; + Log.Info($"MpDataService | useFactory {useFactory} | MpIoNS {MpIoNS}"); + + // conf DB + string connStr = _configuration.GetConnectionString("MP.Data") ?? ""; + if (string.IsNullOrEmpty(connStr)) + { + Log.Error("DbController: ConnString empty!"); + } + else + { + SpecDbController = new MpSpecController(configuration); + IocDbController = new MpIocController(configuration); + Log.Info("DbControllers INIT OK"); + } + + MtcService = mtcServ; + + // conf mongo... + connStr = _configuration.GetConnectionString("mdbConnString") ?? ""; + if (string.IsNullOrEmpty(connStr)) + { + Log.Error("MongoController: ConnString empty!"); + } + else + { + mongoController = new MpMongoController(configuration); + Log.Info("MongoController INIT OK"); + } + + } + + #endregion Public Constructors + + #region Public Properties + + public static MpIocController IocDbController { get; set; } = null!; + public static MpMongoController mongoController { get; set; } = null!; + public static MpSpecController SpecDbController { get; set; } = null!; + public MessagePipe BroadastMsgPipe { get; set; } = null!; + + /// + /// Dizionario dei tag configurati per IOB + /// + public Dictionary> currTagConf { get; set; } = new Dictionary>(); + + #endregion Public Properties + + #region Public Methods + + /// + /// Recupera eventuali azioni richieste + /// + /// + public async Task ActionGetReq() + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + DisplayAction? result = null; + // cerco in redis... + RedisValue rawData = await redisDb.StringGetAsync(Utils.redisActionReq); + if (!string.IsNullOrEmpty($"{rawData}")) + { + result = JsonConvert.DeserializeObject($"{rawData}"); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ActionGetReq Read from REDIS: {ts.TotalMilliseconds}ms"); + } + if (result == null) + { + result = new DisplayAction(); + } + return result; + } + + /// + /// Salva richiesta azione + /// + /// + /// + public bool ActionSetReq(DisplayAction? act2save) + { + bool fatto = false; + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + // cerco in redis... + string rawData = JsonConvert.SerializeObject(act2save); + // invio broadcast + salvo in redis + BroadastMsgPipe.saveAndSendMessage(Utils.redisActionReq, rawData); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ActionSetReq REDIS send to broadcast + Write cache: {ts.TotalMilliseconds}ms"); + return fatto; + } + + /// + /// Verifica se sia da reinviare un tName alla macchina dall'elenco di quelli salvati (in + /// modalità upsert) se non scaduti + /// + /// + /// + /// + /// + public bool AddCheckTask4Machine(string idxMacchina, taskType taskKey, string taskVal) + { + bool answ = false; + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + Log.Info($"addCheckTask4Machine idxMacchina: {idxMacchina} | taskKey: {taskKey} | taskVal: {taskVal}"); + try + { + Dictionary savedTask = mSavedTaskMacchina(idxMacchina); + // cerco valOut saved + string savedVal = savedTask[taskKey.ToString()]; + // se ho un valOut != "" --> rimetto in coda di invio... + if (!string.IsNullOrEmpty(savedVal) && (savedVal != taskVal)) + { + // leggo tName attuali... + Dictionary currTask = mTaskMacchina(idxMacchina); + // rimetto in tName da eseguire... + currTask[taskKey.ToString()] = savedVal; + RedisSetHashDict(currHash, currTask); + answ = true; + Log.Info($"re-issued task4machine: idxMacchina: {idxMacchina} | taskKey: {taskKey} | savedVal: {savedVal}"); + } + } + catch (Exception exc) + { + Log.Error($"Eccezione in AddCheckTask4Machine{Environment.NewLine}{exc}"); + } + + return answ; + } + + /// + /// Aggiunge un PARAMETRO OPZIONALE all'elenco di quelli salvati (in modalità upsert) + /// + /// + /// + /// + /// + public bool AddOptPar4Machine(string idxMacchina, string taskKey, string taskVal) + { + bool answ = false; + RedisKey currHash = Utils.RedKeyOptPar(idxMacchina, MpIoNS); + try + { + // leggo tName attuali... + var currVal = mOptParMacchina(idxMacchina); + // verifico se esista o se vada aggiunto... + if (currVal.ContainsKey(taskKey)) + { + currVal[taskKey] = taskVal; + } + else + { + currVal.Add(taskKey, taskVal); + } + RedisSetHashDict(currHash, currVal); + answ = true; + } + catch (Exception exc) + { + Log.Error($"Eccezione in addOptPar4Machine{Environment.NewLine}{exc}"); + } + return answ; + } + + /// + /// Aggiunge un tName all'elenco di quelli salvati (in modalità upsert) + /// + /// + /// + /// + /// + public bool AddTask4Machine(string idxMacchina, Enums.taskType taskKey, string taskVal) + { + bool answ = false; + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + var currSavedParHash = Utils.RedKeySavedTask2ExeMacc(idxMacchina, MpIoNS); + Dictionary currTask = new Dictionary(); + Dictionary savedTask = new Dictionary(); + try + { + // leggo tName attuali... + currTask = mTaskMacchina(idxMacchina); + if (currTask.ContainsKey($"{taskKey}")) + { + currTask[$"{taskKey}"] = taskVal; + } + else + { + currTask.Add($"{taskKey}", taskVal); + } + RedisSetHashDict(currHash, currTask); + answ = true; + Log.Info($"Task ADD | hash: {currHash} | idxMacchina: {idxMacchina} | taskKey: {taskKey} | taskVal: {taskVal}"); + } + catch { } + // verifico in base al tipo di tName se fare backup... + switch (taskKey) + { + case Enums.taskType.setArt: + case Enums.taskType.setComm: + case Enums.taskType.setPzComm: + case Enums.taskType.setProg: + // leggo tName SALVATI attuali... + savedTask = mSavedTaskMacchina(idxMacchina); + savedTask[taskKey.ToString()] = taskVal; + RedisSetHashDict(currSavedParHash, savedTask); + answ = true; + break; + + case Enums.taskType.endProd: + // salvo un DICT vuoto x resettare + savedTask = new Dictionary(); + RedisSetHashDict(currSavedParHash, savedTask); + answ = true; + break; + + default: + break; + } + return answ; + } + + /// + /// Aggiunge un set di task x macchina all'elenco di quelli salvati (in modalità upsert) + /// + /// + /// + /// + /// + public async Task AddTask4MacListAsync(string idxMacchina, Dictionary taskDict) + { + bool answ = false; + bool needSaveParams = false; + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + var currSavedParHash = Utils.RedKeySavedTask2ExeMacc(idxMacchina, MpIoNS); + Dictionary currTask = new Dictionary(); + Dictionary savedTask = new Dictionary(); + + // leggo valori attuali... + currTask = await mTaskMacchinaAsync(idxMacchina); + // verifico processing dei ricevuti + foreach (var item in taskDict) + { + if (currTask.ContainsKey($"{item.Key}")) + { + currTask[$"{item.Key}"] = item.Value; + } + else + { + currTask.Add($"{item.Key}", item.Value); + } + Log.Trace($"Task ADD | hash: {currHash} | idxMacchina: {idxMacchina} | taskKey: {item.Key} | taskVal: {item.Value}"); + + // verifico in base al tipo di tName se fare backup... + switch (item.Key) + { + case Enums.taskType.setArt: + case Enums.taskType.setComm: + case Enums.taskType.setPzComm: + case Enums.taskType.setProg: + // leggo tName SALVATI attuali... + savedTask = mSavedTaskMacchina(idxMacchina); + savedTask[item.Key.ToString()] = item.Value; + needSaveParams = true; + break; + + case Enums.taskType.endProd: + // salvo un DICT vuoto x resettare + savedTask = new Dictionary(); + needSaveParams = true; + break; + + default: + break; + } + } + // salvo! + await RedisSetHashDictAsync(currHash, currTask); + answ = true; + + if (needSaveParams) + { + await RedisSetHashDictAsync(currSavedParHash, savedTask); + } + return answ; + } + + /// + /// Insert record allarme + /// + /// Data evento + /// Idx macchina + /// area memoria + /// indice memoria + /// valOut status + /// valOut decodificato + /// + public async Task AlarmInsertAsync(DateTime dtRif, string idxMacchina, string memAddress, int memIndex, int statusVal, string valDecoded) + { + // aggiorno record sul DB + bool answ = await IocDbController.AlarmLogInsertAsync(dtRif, idxMacchina, memAddress, memIndex, statusVal, valDecoded); + + return answ; + } + + public async Task> AnagStatiComm() + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + List? result = new List(); + // cerco in redis... + RedisValue rawData = await redisDb.StringGetAsync(Utils.redisStatoCom); + if (!string.IsNullOrEmpty($"{rawData}")) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"AnagStatiComm Read from REDIS: {ts.TotalMilliseconds}ms"); + } + else + { + result = await Task.FromResult(SpecDbController.AnagStatiComm()); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(Utils.redisStatoCom, rawData, getRandTOut(redisLongTimeCache)); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"AnagStatiComm Read from DB: {ts.TotalMilliseconds}ms"); + } + if (result == null) + { + result = new List(); + } + return result; + } + + /// + /// Restituisce l'anagrafica STATI per intero + /// + /// + public async Task> AnagStatiGetAllAsync() + { + List dbResult = new List(); + // cerco in redis... + var currKey = Utils.redisAnagStati; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + dbResult = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + dbResult = await IocDbController.AnagStatiGetAllAsync(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(dbResult); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + return dbResult; + } + + public async Task> AnagTipoArtLV() + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string source = "DB"; + List? result = new List(); + // cerco in redis... + RedisValue rawData = await redisDb.StringGetAsync(Utils.redisTipoArt); + if (!string.IsNullOrEmpty($"{rawData}")) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + source = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.AnagTipoArtLV()); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(Utils.redisTipoArt, rawData, getRandTOut(redisLongTimeCache)); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"AnagTipoArtLV Read from {source}: {ts.TotalMilliseconds}ms"); + if (result == null) + { + result = new List(); + } + return result; + } + + /// + /// Elenco Codice articolo con dati dossier gestiti + /// + /// + public async Task> ArticleWithDossier() + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = Utils.redisArtByDossier; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.ArticleWithDossier()); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ArticleWithDossier | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Eliminazione record selezionato + /// + /// + /// + public async Task ArticoliDeleteRecord(AnagArticoliModel currRec) + { + bool fatto = await SpecDbController.ArticoliDeleteRecord(currRec); + await resetCacheArticoli(); + return fatto; + } + + /// + /// Elenco ultimi articoli data amcchina + /// + /// + /// + public async Task> ArticoliGetLastByMaccAsync(string idxMacc) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisArtList}:Last:{idxMacc}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await IocDbController.ArticoliGetLastByMaccAsync(idxMacc); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ArticoliGetLastByMaccAsync | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Restitusice elenco articoli cercati + /// + /// + /// + /// + public async Task> ArticoliGetSearch(int numRecord, string azienda, string searchVal) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string sKey = string.IsNullOrEmpty(searchVal) ? "***" : searchVal; + string currKey = $"{Utils.redisArtList}:{azienda}:{sKey}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.ArticoliGetSearch(numRecord, azienda, searchVal)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache / 5)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ArticoliGetSearch | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Aggiornamento record selezionato + /// + /// + /// + public async Task ArticoliUpdateRecord(AnagArticoliModel currRec) + { + bool fatto = await SpecDbController.ArticoliUpdateRecord(currRec); + await resetCacheArticoli(); + return fatto; + } + + /// + /// Verifica se sia possiubile cancellare articolo dato suo CodArt cercando su redis o su + /// tab veto da DB + /// + /// + /// + public bool ArticoloDelEnabled(object CodArt) + { + bool answ = false; + string codArticolo = $"{CodArt}"; + int cacheCheckArtUsato = 1; + int.TryParse(_configuration.GetValue("ServerConf:cacheCheckArtUsato"), out cacheCheckArtUsato); + TimeSpan TTLCache = getRandTOut(cacheCheckArtUsato); + // cerco in cache redis... + string redKeyArtUsed = $"{Utils.redKeyArtUsed}:{codArticolo}"; + string redKeyTabCheckArt = Utils.redKeyTabCheckArt; + var rawData = redisDb.StringGet(redKeyArtUsed); + if (!string.IsNullOrEmpty(rawData)) + { + bool.TryParse(rawData, out answ); + } + else + { + // controllo non sia stato mai prodotto sennò non posso cancellare... + try + { + // cerco in cache se ci sia la tabella con gli articoli impiegati... + var rawTable = redisDb.StringGet(redKeyTabCheckArt); + List? artList = new List(); + if (!string.IsNullOrEmpty(rawTable)) + { + artList = JsonConvert.DeserializeObject>($"{rawTable}"); + } + // rileggo... + if (artList == null || artList.Count == 0) + { + artList = new List(); + var tabArticoli = SpecDbController.ArticoliGetUsed(); + var codList = tabArticoli.Select(x => x.CodArticolo); + foreach (string cod in codList) + { + artList.Add(cod); + } + // SE fosse vuoto aggiungo comunque il cado "ND"... + if (artList.Count == 0) + { + artList.Add("ND"); + } + // salvo + rawTable = JsonConvert.SerializeObject(artList); + redisDb.StringSet(redKeyTabCheckArt, rawTable, TTLCache); + } + // cerco nella tabella: se ci fosse --> disabilitato delete + bool usato = false; + if (artList != null && artList.Count > 0) + { + usato = artList.Contains(codArticolo); + } + answ = !usato; + redisDb.StringSet(redKeyArtUsed, $"{answ}", TTLCache); + } + catch + { } + } + return answ; + } + + /// + /// Effettua split ODL + /// + /// macchina + /// effettuare conferma qty + /// imposta la qty prox ODL da ODL che si chiude + /// matricola operatore + /// Round Step quantità prox ODL (corrente come riferimento) + /// Cod ext da associare all'ODL + /// + public async Task AutoStartOdlAsync(string idxMacchina, bool doConfirm, bool qtyFromLast, int matrOpr, int roundStep = 100, string keyRichiesta = "") + { + string answ = "KO"; + DateTime adesso = DateTime.Now; + + // verifico NON CI SIA un veto a NUOVI split... + string redKey = $"{Utils.redisVetoSplitOdl}:{idxMacchina}"; + var rawData = await redisDb.StringGetAsync(redKey); + if (rawData.HasValue) + { + Log.Info($"VETO ATTIVO | Richiesto forceSplitOdl per impianto {idxMacchina} | impossibile procedere"); + } + else + { + // registro VETO x altri split... 5 minuti + await redisDb.StringSetAsync(redKey, $"Inizio SPLIT-ODL {adesso}", TimeSpan.FromMinutes(5)); + // setup dati da config + bool confRett = false; + int modoConfProd = -1; + bool slaveConfirmPzProd = false; + var configData = await ConfigGetAllAsync(); + if (configData != null) + { + var currRec = configData.FirstOrDefault(x => x.Chiave == "confRett"); + if (currRec != null) + { + bool.TryParse(currRec.Valore, out confRett); + } + currRec = configData.FirstOrDefault(x => x.Chiave == "modoConfProd"); + if (currRec != null) + { + int.TryParse(currRec.Valore, out modoConfProd); + } + currRec = configData.FirstOrDefault(x => x.Chiave == "SlaveConfirmPzProd"); + if (currRec != null) + { + bool.TryParse(currRec.Valore, out slaveConfirmPzProd); + } + } + // calcolo la qta da gestire + int qtyConf = 0; + int qtyNew = 0; + int qtyScarto = 0; + if (doConfirm) + { + try + { + var rigaProd = await IocDbController.PezziProdMacchinaAsync(idxMacchina); + var statoProd = await IocDbController.StatoProdMacchinaAsync(idxMacchina, adesso); + qtyConf = rigaProd.pezziNonConfermati; + qtyScarto = statoProd.Pz2RecScarto; + // calcolo nuovi pezzi da confermare + if (qtyFromLast) + { + roundStep = roundStep == 0 ? 1 : roundStep; + double ratio = (double)qtyConf / roundStep; + qtyNew = (int)Math.Round(Math.Ceiling(ratio) * roundStep, 0); + } + } + catch (Exception exc) + { + Log.Error($"AutoStartOdlAsync | Errore recupero pezzi da confermare | idxMacchina {idxMacchina}{Environment.NewLine}{exc}"); + } + } + + // chiamo metodo redis/db... + try + { + // recupero ODL corrente + var currData = await IocDbController.OdlCurrByMaccAsync(idxMacchina); + if (currData != null && currData.IdxOdl > 0) + { + // preparo var x master/slave + bool isMachMaster = await IobIsMasterAsync(idxMacchina); + List slaveList = new(); + if (isMachMaster) + { + List allSlaveList = await IocDbController.Macchine2SlaveAsync(); + slaveList = allSlaveList.Where(x => x.IdxMacchina == idxMacchina).ToList(); + } + + // registro un evento di inizio attrezzaggio (idxTipoEv = 2) + int idxEvento = 2; + Log.Info($"Invio evento ODL-SPLIT per macchina {idxMacchina} | ev: {idxEvento} | art: {currData.CodArticolo} | qty conf: {qtyConf} | sty sca: {qtyScarto} | new qty: {qtyNew}"); + + // creo evento + EventListModel newRecEv = new EventListModel() + { + CodArticolo = currData.CodArticolo, + IdxMacchina = idxMacchina, + IdxTipo = idxEvento, + InizioStato = adesso, + MatrOpr = matrOpr, + pallet = "", + Value = "ODL-SPLIT" + }; + inputComandoMapo resCmd = await scriviRigaEventoBarcodeAsync(newRecEv); + + // era 100, messo 50... + int wTime = 50; + + // eventuale conferma produzione + if (doConfirm) + { + await Task.Delay(wTime); + adesso = DateTime.Now; + // chiamo conferma produzione... + try + { + string chiamata = confRett ? "confermaProdMacchinaFull" : "confermaProdMacchina"; + Log.Info($"Chiamata a {chiamata} con parametri {currData.IdxMacchina} |{matrOpr} | {DateTime.Now.AddDays(-10)} | {DateTime.Now} | {qtyConf} | 0 | {qtyScarto} | {DateTime.Now} | false"); + string idxMacchinaSel = currData.IdxMacchina; + bool fatto = false; + if (confRett) + { + // confermo al netto dei pezzi lasciati... + fatto = await IocDbController.ConfermaProdMacchinaFullAsync(idxMacchinaSel, modoConfProd, qtyConf, 0, qtyScarto, adesso, matrOpr); + if (slaveConfirmPzProd) + { + foreach (var machine in slaveList) + { + await IocDbController.ConfermaProdMacchinaFullAsync(machine.IdxMacchinaSlave, modoConfProd, qtyConf, 0, qtyScarto, adesso, matrOpr); + } + } + } + else + { + fatto = await IocDbController.ConfermaProdMacchinaAsync(idxMacchinaSel, modoConfProd, qtyConf, qtyScarto, adesso, matrOpr); + if (slaveConfirmPzProd) + { + foreach (var machine in slaveList) + { + await IocDbController.ConfermaProdMacchinaAsync(idxMacchinaSel, modoConfProd, qtyConf, qtyScarto, adesso, matrOpr); + } + } + } + if (!fatto) + { + Log.Error($"ERRORE in chiamata {chiamata}"); + } + } + catch (Exception exc) + { + Log.Error($"Eccezione in ConfermaProduzione{Environment.NewLine}{exc}"); + } + } + // 2025.02.19 portato da 100 a 1000 ms attesa x evitare che ODL sia avviato PRIMA della conferma + // attendo 1000 msec x chiudere ODL + await Task.Delay(1000); + // chiamo splitOdl + await IocDbController.AutoStartOdlAsync(currData.IdxOdl, 0, idxMacchina, currData.TCRichAttr, currData.PzPallet, $"Nuovo ODL da forceSplitOdl.autoStart", true, qtyNew, keyRichiesta); + + // elimino eventuale ODL precedente... + string currKey = $"{Utils.redisOdlCurrByMac}:{idxMacchina}"; + await redisDb.KeyDeleteAsync(currKey); + + // ricalcola ODL macchina + var newOdl = await GetCurrOdlAsync(idxMacchina); + // attendo 1000 msec + await Task.Delay(1000); + + adesso = DateTime.Now; + // registro fine ODL (idxTipoEv = 1) + idxEvento = 1; + Log.Info($"Invio evento FINE ODL-SPLIT | idx: {idxMacchina} | ev: {idxEvento} | art: {currData.CodArticolo}"); + // costruisco nuovo evento + newRecEv = new EventListModel() + { + CodArticolo = currData.CodArticolo, + IdxMacchina = idxMacchina, + IdxTipo = idxEvento, + InizioStato = adesso, + MatrOpr = matrOpr, + pallet = "", + Value = "ODL-START" + }; + resCmd = await scriviRigaEventoBarcodeAsync(newRecEv); + // invio eventi setup a macchina.... + string setArtVal = $"{currData.CodArticolo}"; + string setPzCommVal = $"{qtyNew}"; + string setCommVal = $"ODL{newOdl}"; + if (!string.IsNullOrEmpty(keyRichiesta)) + { + setCommVal = $"{keyRichiesta} ODL{newOdl}"; + } + try + { + // invio task caricamento dati ODL + Dictionary updDict = new Dictionary(); + updDict.Add(taskType.setArt, setArtVal); + updDict.Add(taskType.setComm, setCommVal); + updDict.Add(taskType.setPzComm, setPzCommVal); +#if false + addTask4Machine(idxMacchina, taskType.setArt, setArtVal); + addTask4Machine(idxMacchina, taskType.setComm, setCommVal); + addTask4Machine(idxMacchina, taskType.setPzComm, setPzCommVal); + + updateMachineParameter(idxMacchina, "setArt", setArtVal); + updateMachineParameter(idxMacchina, "setComm", setCommVal); + updateMachineParameter(idxMacchina, "setPzComm", setPzCommVal); +#endif + // recupero set attuale parametri + var currMachPar = await MachineParamListAsync(idxMacchina); + List list2upd = new(); + // aggiorno i valori interessati tra quelli nel dizionario + foreach (var item in updDict) + { + var cRec = currMachPar.FirstOrDefault(x => x.uid == $"{item.Key}"); + if (cRec != null) + { + list2upd.Add(cRec); + } + } + // aggiungo task di setParameters x la commessa... + updDict.Add(taskType.setParameter, setCommVal); + // salvo + await AddTask4MacListAsync(idxMacchina, updDict); + await MachineParamUpsertAsync(idxMacchina, list2upd); + } + catch + { } + // chiamo refresh MSE + await IocDbController.RecalcMseAsync(idxMacchina, 0); + // resetto stato macchina... + var kStatoMacc = $"{Utils.redisStatoMacch}:{idxMacchina}"; + await redisDb.KeyDeleteAsync(kStatoMacc); + answ = "OK"; + Log.Info($"Effettuato reset e ricalcoli x split ODL per macchina {idxMacchina}"); + // se è una master richiamo fix x child... + if (isMachMaster) + { + Dictionary updDict = new Dictionary(); + string ts = ""; + string outData = ""; + await IocDbController.OdlFixMachineSlaveAsync(idxMacchina, 30, 1); + foreach (var machine in slaveList) + { + // invio chiusura attrezzaggio + ts = string.Format("{0:yyMMdd}T{0:HHmmss.fff}Z", DateTime.Now); + outData = $"TS:{ts}|MATR:{matrOpr}|ODL:{newOdl}"; + + updDict.Clear(); + updDict.Add(taskType.setArt, setArtVal); + updDict.Add(taskType.setComm, setCommVal); + updDict.Add(taskType.setPzComm, setPzCommVal); + +#if false + addTask4Machine(machine.IdxMacchinaSlave, taskType.fixStopSetup, outData); + addTask4Machine(machine.IdxMacchinaSlave, taskType.forceResetPzCount, outData); + // invio task caricamento dati ODL + addTask4Machine(machine.IdxMacchinaSlave, taskType.setArt, setArtVal); + addTask4Machine(machine.IdxMacchinaSlave, taskType.setComm, setCommVal); + addTask4Machine(machine.IdxMacchinaSlave, taskType.setPzComm, setPzCommVal); + + updateMachineParameter(machine.IdxMacchinaSlave, "setArt", setArtVal); + updateMachineParameter(machine.IdxMacchinaSlave, "setComm", setCommVal); + updateMachineParameter(machine.IdxMacchinaSlave, "setPzComm", setPzCommVal); +#endif + + // recupero set attuale parametri + var currMachPar = await MachineParamListAsync(machine.IdxMacchinaSlave); + List list2upd = new(); + // aggiorno i valori interessati tra quelli nel dizionario + foreach (var item in updDict) + { + var cRec = currMachPar.FirstOrDefault(x => x.uid == $"{item.Key}"); + if (cRec != null) + { + list2upd.Add(cRec); + } + } + // aggiungo task di setParameters x la commessa... + updDict.Add(taskType.fixStopSetup, outData); + updDict.Add(taskType.forceResetPzCount, outData); + updDict.Add(taskType.setParameter, setCommVal); + // salvo + await AddTask4MacListAsync(machine.IdxMacchinaSlave, updDict); + await MachineParamUpsertAsync(machine.IdxMacchinaSlave, list2upd); + } + } + } + } + catch (Exception exc) + { + Log.Error($"Eccezione in forceSplitOdl{Environment.NewLine}{exc}"); + } + } + return answ; + } + + public string CalcRecipe(RecipeModel currRecipe) + { + return mongoController.CalcRecipe(currRecipe); + } + + /// + /// 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) + /// + public 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 IocDbController.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 + { + if (useFactory) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var iocService = scope.ServiceProvider.GetRequiredService(); + // solo microstato + await iocService.MicroStatoMacchinaUpsertAsync(newRecMsm); + } + else + { + // solo microstato + await IocDbController.MicroStatoMacchinaUpsertAsync(newRecMsm); + } + } + return answ; + } + + public async Task> ConfigGetAllAsync() + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + List? result = new List(); + // cerco in redis... + RedisValue rawData = await redisDb.StringGetAsync(Utils.redisConfKey); + if (!string.IsNullOrEmpty($"{rawData}")) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ConfigGetAllAsync Read from REDIS: {ts.TotalMilliseconds}ms"); + } + else + { + result = await IocDbController.ConfigGetAllAsync(); + //result = await Task.FromResult(SpecDbController.ConfigGetAllAsync()); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(Utils.redisConfKey, rawData, getRandTOut(redisLongTimeCache)); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ConfigGetAllAsync Read from DB: {ts.TotalMilliseconds}ms"); + } + if (result == null) + { + result = new List(); + } + return result; + } + + /// + /// Reset dati cache config + /// + /// + public async Task ConfigResetCache() + { + await redisDb.StringSetAsync(Utils.redisConfKey, ""); + } + + /// + /// Update chiave config + /// + /// + public async Task ConfigUpdate(ConfigModel updRec) + { + return await Task.FromResult(SpecDbController.ConfigUpdate(updRec)); + } + + /// + /// Elenco completo valori DatiMacchine + /// + /// + public async Task> DatiMacchineGetAll() + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisBaseAddr}:TabDatiMacchine:ALL"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.DatiMacchineGetAll()); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"DatiMacchineGetAll | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + public async Task> DecNumArtGetFiltAsync(string codArt = "") + { + List result = new(); + string tag = string.IsNullOrEmpty(codArt) ? "ALL" : codArt; + string currKey = $"{Utils.redisDecNumArtKey}:{tag}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + result = await IocDbController.DecNumArtGetFiltAsync(codArt); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + return result; + } + + /// + /// Dispose del connettore ai dati + /// + public void Dispose() + { + // Clear database controller + SpecDbController.Dispose(); + mongoController.Dispose(); + redisConn.Dispose(); + } + + /// + /// Restituisce l'elenco delle date dei dossier x una macchina (se presenti) Impiegata anche + /// cache redis + /// + /// + /// + public async Task> DossierLastByMachAsync(string idxMacchina) + { + List result = new List(); + + var currKey = $"{Utils.redisDossByMacLast}:{idxMacchina}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + result = await IocDbController.DossGetLastByMaccAsync(idxMacchina); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + return result; + } + + /// + /// Eliminazione di un dossier + /// + /// record dossier da eliminare + /// + public async Task DossiersDeleteRecord(DossierModel selRecord) + { + bool result = false; + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + result = await SpecDbController.DossiersDeleteRecord(selRecord); + // elimino cache redis... + RedisValue pattern = new RedisValue($"{Utils.redisDossByMac}:*"); + bool answ = await RedisFlushPatternAsync(pattern); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"DossiersDeleteRecord | IdxMacchina {selRecord.IdxMacchina} | DtRif {selRecord.DtRif} | IdxODL {selRecord.IdxODL} | {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Elenco ultimi n record DOssiers (che contengono ad esempio "salvataggi" di FLuxLog) dato + /// macchina (ordinato x data registrazione) + /// + /// * = tutte, altrimenti solo x una data macchina + /// Data minima per estrazione records + /// Data Massima per estrazione records + /// Num Max records da recuperare + /// + public async Task> DossiersGetLastFilt(string IdxMacchina, string CodArticolo, DateTime DtStart, DateTime DtEnd, int MaxRec) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisDossByMac}:{IdxMacchina}:{CodArticolo}:{DtStart:yyyyMMddHHmm}:{DtEnd:yyyyMMddHHmm}:{MaxRec}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await SpecDbController.DossiersGetLastFiltAsync(IdxMacchina, CodArticolo, DtStart, DtEnd, MaxRec); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache / 5)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"DossiersGetLastFilt | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Inserimento nuovo record dossier + /// + /// + /// + public async Task DossiersInsert(DossierModel currDoss) + { + // aggiorno record sul DB + bool answ = await SpecDbController.DossiersInsert(currDoss); + + return answ; + } + + /// + /// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache redis + /// + /// macchina + /// NUm massimo secondi per recuperare dati correnti + /// DataOra riferimento x cui prendere valori antecedenti + /// + public async Task DossiersTakeParamsSnapshotLast(string IdxMacchina, DateTime dtMin, DateTime dtMax) + { + bool answ = false; + await Task.Delay(1); + Log.Info($"Richiesta snapshot per macchina {IdxMacchina} | periodo {dtMin} --> {dtMax}"); + // chiamo stored x salvare parametri + SpecDbController.DossiersTakeParamsSnapshotLast(IdxMacchina, dtMin, dtMax); + // elimino cache redis... + RedisValue pattern = new RedisValue($"{Utils.redisDossByMac}:*"); + answ = await RedisFlushPatternAsync(pattern); + Log.Info($"Svuotata cache dossier | {pattern}"); + return answ; + } + + /// + /// Update valOut dossier + /// + /// + /// + public async Task DossiersUpdateValore(DossierModel currDoss) + { + // aggiorno record sul DB + bool answ = await SpecDbController.DossiersUpdateValore(currDoss); + + return answ; + } + + /// + /// Restitusice elenco aziende + /// + /// + public Task> ElencoAziende() + { + return Task.FromResult(SpecDbController.AnagGruppiAziende()); + } + + /// + /// Restitusice elenco fasi + /// + /// + public Task> ElencoGruppiFase() + { + List result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisAnagGruppi}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + var rawResult = JsonConvert.DeserializeObject>($"{rawData}"); + if (rawResult != null) + { + result = rawResult; + } + readType = "REDIS"; + } + else + { + result = SpecDbController.AnagGruppiFase(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache / 5)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ElencoGruppiFase | Read from {readType}: {ts.TotalMilliseconds}ms"); + return Task.FromResult(result); + } + + public Task> ElencoLink() + { + return Task.FromResult(SpecDbController.ElencoLink()); + } + + /// + /// Aggiunta record EventList + /// + /// + /// + public async Task EvListInsert(EventListModel newRec) + { + return await SpecDbController.EvListInsert(newRec); + } + + /// + /// Imposta in redis la scadenza della pagina x il reload + /// + /// + /// + public DateTime ExpiryReloadParamGet() + { + DateTime dtRif = DateTime.Now; + string currKey = $"{Utils.redisParamPageExp}"; + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + dtRif = JsonConvert.DeserializeObject($"{rawData}"); + } + return dtRif; + } + + /// + /// Imposta in redis la scadenza della pagina x il reload + /// + /// + /// + public bool ExpiryReloadParamSet(DateTime expTime) + { + bool fatto = false; + string currKey = $"{Utils.redisParamPageExp}"; + string rawData = JsonConvert.SerializeObject(expTime); + fatto = redisDb.StringSet(currKey, rawData); + return fatto; + } + + /// + /// Task completo sistemazione dossier quotidiani mancanti + /// + /// + /// + public async Task FixDailyDossierAsync(string idxMacc) + { + string answ = ""; + // verifico se si possa processare, ovvero tab ConfFlux x macchina sia valorizzata... + var confDataMach = await ConfFluxMach(idxMacc); + if (confDataMach.Count > 0) + { + // determino ultima data da processare (inizio oggi, a mezzanotte) + DateTime dtTo = DateTime.Today; + DateTime dtFrom = dtTo; + // determino data di partenza, prima da dossier esistenti + var listaDoss = await DossierLastByMachAsync(idxMacc); + if (listaDoss.Count > 0) + { + // primo giorno DOPO ultima registrazione + dtFrom = listaDoss + .Select(x => x.DtRif) + .OrderByDescending(x => x) + .FirstOrDefault() + .AddDays(1); + } + else + { + // ...o da fluxLog acquisiti... + var listaFL = await FluxLogFirstByMachAsync(idxMacc); + if (listaFL.Count > 0) + { + // giorno successivo a prima registrazione + dtFrom = listaFL + .OrderBy(x => x) + .FirstOrDefault() + .AddDays(1); + } + } + string caller = $"takeFlogSnapshot({idxMacc})"; + DateTime dtStart = dtFrom.Date; + DateTime dtEnd = dtFrom; + // max 10 dossier alla volta (se non configurato diversamente) + int maxAdd = 1; + string confVal = await tryGetConfigAsync("IO_numDossMaxCreate"); + if (!string.IsNullOrEmpty(confVal)) + { + int.TryParse(confVal, out maxAdd); + } + + if (dtStart < dtTo) + { + // verifico di avere almeno 1 dossier da produrre ciclo fino ad esaurire le + // date da processare + while (dtStart < dtTo && maxAdd > 0) + { + // sistemo end + dtEnd = dtStart.AddDays(1); + // effettuo chiamata registrazione snapshot! + answ = await FluxLogSaveSnapshotAsync(idxMacc, dtStart, dtEnd, caller); + // incremento START... + dtStart = dtEnd; + // riduco il numero di chiamate ammesse x singolo task + maxAdd--; + } + // reset cache dossier... + await DossierLastByMachResetAsync(idxMacc); + answ = "OK"; + } + else + { + Log.Warn("FixDailyDossierAsync | NO more to add"); + } + } + else + { + Log.Warn("FixDailyDossierAsync | NO ConfFluxData"); + } + return answ; + } + + public async Task FlushRedisCache() + { + await Task.Delay(1); + RedisValue pattern = Utils.RedValue("*"); + bool answ = await RedisFlushPatternAsync(pattern); + // rileggo vocabolario.,.. + ObjVocabolario = VocabolarioGetAll(); + return answ; + } + + public async Task FlushRedisKey(string redKey) + { + await Task.Delay(1); + RedisValue pattern = Utils.RedValue(redKey); + bool answ = await RedisFlushPatternAsync(pattern); + return answ; + } + + public List FluxLogDtoGetByFlux(string Valore) + { + List answ = new List(); + DossierFluxLogDTO? result = JsonConvert.DeserializeObject(Valore); + if (result != null) + { + if (result.ODL != null) + { + answ = result + .ODL + .OrderBy(x => x.CodFlux) + .ToList(); + // inizializzo SE necessario + foreach (var item in answ) + { + item.ValoreEdit = String.IsNullOrEmpty(item.ValoreEdit) ? item.Valore : item.ValoreEdit; + } + } + } + return answ; + } + + /// + /// Elenco ultimi n record flux log dato macchina e flusso (ordinato x data registrazione) + /// + /// Data massima x eventi + /// Data minima x eventi + /// * = tutte, altrimenti solo x una data macchina + /// *=tutti, altrimenti solo selezionato + /// numero massimo record da restituire + /// + public async Task> FluxLogGetLastFilt(DateTime DtMax, DateTime DtMin, string IdxMacchina, string CodFlux, int MaxRec, double redisCacheSec) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisFluxLogFilt}:{IdxMacchina}:{CodFlux}:{MaxRec}:{DtMax:yyyyMMddHHmm}:{DtMin:yyyyMMddHHmm}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.FluxLogGetLastFilt(DtMax, DtMin, IdxMacchina, CodFlux, MaxRec)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + if (string.IsNullOrEmpty(canCacheParametri)) + { + canCacheParametri = await tryGetConfigAsync("SPEC_ParametriEnableRedisCache"); + } + if (canCacheParametri != "false") + { + redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisCacheSec)); + } + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"FluxLogGetLastFilt | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Restituisce il valOut dell'ODL corrente (ODL deve esserci per gestione contapezzi, senza + /// ODL NO invio/gestione ODL) + /// + /// + /// + public async Task GetCurrOdlAsync(string idxMacchina) + { + string result = ""; + string currKey = $"{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; + } + + /// + /// Restituisce il valOut dell'ODL corrente (ODL deve esserci per gestione contapezzi, senza + /// ODL NO invio/gestione ODL) + /// + /// + /// indica se forzare lettura da db (true) o meno + /// + public async Task GetCurrOdlAsync(string idxMacchina, bool forceDb) + { + string answ = ""; + // se ho forceDB leggo dai dati prod... + if (forceDb) + { + var datiProd = await StatoProdMacchinaAsync(idxMacchina, DateTime.Now, true); + if (datiProd != null) + { + answ = datiProd.IdxOdl.ToString(); + } + } + else + { + answ = await GetCurrOdlAsync(idxMacchina); + } + return answ; + } + + /// + /// Restituisce il valOut dell'ultimo ODL + /// + /// + /// + public async Task GetLastOdlAsync(string idxMacchina) + { + ODLExpModel result = new ODLExpModel(); + string currKey = $"{Utils.redisOdlLastByMac}:{idxMacchina}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject($"{rawData}") ?? new(); + } + else + { + result = await IocDbController.OdlLastByMaccAsync(idxMacchina); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + return result; + } + + /// + /// Effettua calcolo data-ora di riferimento per il server a partire da + /// + /// + /// + /// + public DateTime GetSrvDtEvent(string dtEve, string dtCurr) + { + DateTime dataOraEvento = DateTime.Now; + // 2017.09.14 trimmo eventualmente lo zero finale dalle date SE supera i millisecondi... + dtEve = dtEve.Length > 17 ? dtEve.Substring(0, 17) : dtEve; + dtCurr = dtCurr.Length > 17 ? dtCurr.Substring(0, 17) : dtCurr; + DateTime dtEvento, dtCorrente; + // controllo: se ho valori dt x evento e orario DIVERSI per acquisitore IOB calcolo + // dataOraEvento corretto + if (dtEve != dtCurr) + { + Int64 delta = 0; + try + { + // se ho meno decimali x evento rispetto dtCorrente... + if (dtEve.Length < dtCurr.Length) + { + dtEve = dtEve.PadRight(dtCurr.Length, '0'); + } + delta = Convert.ToInt64(dtCurr) - Convert.ToInt64(dtEve); + // se meno di 60'000 ms ... + if (delta < 59999) + { + dataOraEvento = dataOraEvento.AddMilliseconds(-delta); + } + else + { + // in questo caso elimino i MS dalle stringhe e converto i datetime.... + CultureInfo provider = CultureInfo.InvariantCulture; + string format = "yyyyMMddHHmmssfff"; + dtEvento = DateTime.ParseExact(dtEve, format, provider); + dtCorrente = DateTime.ParseExact(dtCurr, format, provider); + TimeSpan deltaTS = dtCorrente.Subtract(dtEvento); + dataOraEvento = dataOraEvento.Add(-deltaTS); + } + } + catch (Exception exc) + { + Log.Error($"getSrvDtEvent | Errore calcolo ora corrente da IOB remoto | dtEve: {dtEve} | dtCurr: {dtCurr}{Environment.NewLine}" + + $"{exc}"); + } + } + + return dataOraEvento; + } + + /// + /// Restitusice elenco KVP dei TASK (da passare a IOB-WIN) per l'impianto indicato + /// + /// + /// + public async Task> GetTask2ExeMacchinaAsync(string idxMacchina) + { + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + return await RedisGetHashDictAsync(currHash); + } + + /// + /// Init ricetta + /// + /// + /// + /// + /// + public RecipeModel InitRecipe(string confPath, int idxPODL, Dictionary CalcArgs) + { + return mongoController.InitRecipe(confPath, idxPODL, CalcArgs); + } + + /// + /// Restituisce il valOut booleano se la macchina sia abilitata all'input + /// + /// + /// + public async Task IobInsEnabAsync(string idxMacchina) + { + var key = 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"); + } + + /// + /// Restituisce il valOut booleano se la macchina sia master + /// + /// + /// + public async Task IobIsMasterAsync(string idxMacchina) + { + var key = Utils.RedKeyDatiMacc(idxMacchina, MpIoNS); + + // 1. Tentativo ottimizzato: leggiamo solo il campo che ci serve + // Supponendo che tu usi StackExchange.Redis direttamente o un wrapper + string? val = await redisDb.HashGetAsync(key, "Master"); + + // 2. Se non c'è in cache, carichiamo/resettiamo tutto + if (val == null) + { + //var data = ResetDatiMacchina(idxMacchina); + var data = await ResetDatiMacchinaAsync(idxMacchina); + data.TryGetValue("Master", out val); + } + + // 3. Parsing sicuro + return val != null && (val == "1" || val.ToLower() == "true"); + } + + /// + /// 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 = 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; + } + + /// + /// + /// idxMacc odl da cercare + /// + public async Task> ListGiacenze(int IdxOdl) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisGiacenzaList}:{IdxOdl}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.ListGiacenze(IdxOdl)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisShortTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ListGiacenze | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + public async Task> ListValuesFilt(string tabName, string fieldName) + { + List resultList = new List(); + string tag = ""; + tag += string.IsNullOrEmpty(tabName) ? "" : $"{tabName}:"; + tag += string.IsNullOrEmpty(fieldName) ? "" : $"{fieldName}:"; + var currKey = $"{Utils.redisConfFlux}:{tag}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + resultList = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + resultList = await IocDbController.ListValuesFiltAsync(tabName, fieldName); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(resultList); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (resultList == null) + { + resultList = new(); + } + return resultList; + } + + /// + /// Elenco completo valori Macchine 2 Slave + /// + /// + public async Task> Macchine2SlaveGetAllAsync() + { + List? result = new List(); + string currKey = $"{Utils.redisBaseAddr}:M2STab"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + } + else + { + if (useFactory) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var mtcService = scope.ServiceProvider.GetRequiredService(); + result = await mtcService.Macchine2SlaveAsync(); + } + else + { + result = await IocDbController.Macchine2SlaveAsync(); + } + + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); + } + if (result == null) + { + result = new List(); + } + return result; + } + + /// + /// Elenco di tutte le macchine gestite + /// + /// + /// + public async Task> MacchineGetFilt(string codGruppo) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string keyGrp = codGruppo != "*" ? codGruppo : "ALL"; + string currKey = $"{Utils.redisMacList}:{keyGrp}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.MacchineGetFilt(codGruppo)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"MacchineGetAll | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Verifica se la macchina abbia un codice PATH ricette associato + /// + /// + /// + public async Task MacchineRecipeArchive(string idxMacchina) + { + string? result = ""; + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisMacRecipePath}:{idxMacchina}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject($"{rawData}"); + readType = "REDIS"; + } + else + { + //recupero elenco macchine... + var machineList = await MacchineGetFilt("*"); + var currMach = machineList.Where(x => x.IdxMacchina == idxMacchina).FirstOrDefault(); + result = currMach != null ? currMach.RecipeArchivePath : null; + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"MacchineRecipeArchive | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result ?? ""; + } + + /// + /// Verifica se la macchina abbia un codice CONF ricetta associato + /// + /// + /// + public async Task MacchineRecipeConf(string idxMacchina) + { + string? result = ""; + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisMacRecipeConf}:{idxMacchina}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject($"{rawData}"); + readType = "REDIS"; + } + else + { + //recupero elenco macchine... + var machineList = await MacchineGetFilt("*"); + var currMach = machineList.Where(x => x.IdxMacchina == idxMacchina).FirstOrDefault(); + result = currMach != null ? currMach.RecipePath : null; + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"MacchineRecipeConf | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result ?? ""; + } + + /// + /// Elenco idxMacc Macchine che abbiano dati FLuxLog, nel periodo indicato + /// + /// + /// + /// + public async Task> MacchineWithFlux(DateTime dtStart, DateTime dtEnd) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisMacByFlux}:{dtStart:yyyyMMddHHmm}:{dtEnd:yyyyMMddHHmm}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await SpecDbController.MacchineWithFlux(dtStart, dtEnd); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"MacchineWithFlux | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Lista parametri correnti (ObjItemDTO) della macchina (ex getCurrObjItems) + /// + /// + /// + public List MachineParamList(string idxMacchina) + { + // setup parametri costanti + string source = "NA"; + Stopwatch sw = new Stopwatch(); + sw.Start(); + List? result = new List(); + // cerco in _redisConn... + var currKey = Utils.RedKeyCurrObjItems(idxMacchina, MpIoNS); + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue && rawData.Length() > 2) + { + var rawVal = JsonConvert.DeserializeObject>($"{rawData}"); + // ordino! + result = rawVal + .OrderBy(x => x.displOrdinal) + .ThenBy(x => x.description) + .ToList(); + source = "REDIS"; + } + if (result == null) + { + result = new List(); + source = "NONE"; + } + sw.Stop(); + Log.Debug($"MachineParamList | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Lista parametri correnti (ObjItemDTO) della macchina (ex getCurrObjItems) + /// + /// + /// + public async Task> MachineParamListAsync(string idxMacchina) + { + // setup parametri costanti + string source = "NA"; + Stopwatch sw = new Stopwatch(); + sw.Start(); + List? result = new List(); + // cerco in _redisConn... + var currKey = Utils.RedKeyCurrObjItems(idxMacchina, MpIoNS); + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue && rawData.Length() > 2) + { + var rawVal = JsonConvert.DeserializeObject>($"{rawData}"); + // ordino! + result = rawVal + .OrderBy(x => x.displOrdinal) + .ThenBy(x => x.description) + .ToList(); + source = "REDIS"; + } + if (result == null) + { + result = new List(); + source = "NONE"; + } + sw.Stop(); + Log.Debug($"MachineParamListAsync | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Lista parametri correnti che necessitano di write della macchina (ex getCurrObjItems) + /// + /// + /// + public async Task> MachineParamListPendingWriteAsync(string idxMacchina) + { + List? result = new List(); + // recupero tutti i parametri + var allData = await MachineParamListAsync(idxMacchina); + result = allData.Where(x => x.writable && !string.IsNullOrEmpty(x.reqValue)).ToList(); + return result; + } + + /// + /// Esegue aggiornamento MachineParamList (ex CurrObjItems) Async + /// + /// + /// + /// + public async Task MachineParamListSetAsync(string idxMacchina, List currData) + { + string serVal = JsonConvert.SerializeObject(currData); + var currKey = Utils.RedKeyCurrObjItems(idxMacchina, MpIoNS); + bool fatto = await redisDb.StringSetAsync(currKey, serVal); + return fatto; + } + + /// + /// Effettua UPSERT elenco parametri correnti x IOB (se c'è UPDATE, se manca ADD) + /// + /// + /// + /// + public async Task MachineParamUpsertAsync(string idxMacchina, List innovations) + { + bool answ = false; + if (innovations != null) + { + Log.Info($"upsertCurrObjItems | idxMacchina: {idxMacchina} | {innovations.Count} innovations"); + // leggo i valori attuali... + List actValues = await MachineParamListAsync(idxMacchina); + // per ogni valOut passatomi faccio insert o update rispetto elenco valori correnti + // in REDIS + foreach (var item in actValues) + { + // cerco nelle innovazioni SE CI SIA il valOut... + var trovato = innovations.Find(obj => obj.uid == item.uid); + // se non trovato nelle innovazioni... + if (trovato == null) + { + // lo ri-aggiungo x non perderlo + innovations.Add(item); + // in base a value (value == null) uso debug/info + if (string.IsNullOrEmpty(item.value)) + { + Log.Debug($"idxMacchina: {idxMacchina} | innovations | add | item.uid: {item.uid} | item.value: {item.value}"); + } + else + { + Log.Info($"idxMacchina: {idxMacchina} | innovations | add | item.uid: {item.uid} | item.value: {item.value}"); + } + } + // altrimenti aggiorno campo (non trasmesso) name e tengo il resto... + else + { + trovato.name = item.name; + Log.Debug($"idxMacchina: {idxMacchina} | innovations | update | item.uid: {item.uid} | item.value: {item.value} --> {trovato.value} "); + } + } + // serializzo e salvo + answ = await MachineParamListSetAsync(idxMacchina, innovations); + } + return answ; + } + + /// + /// Restitusice elenco KVP dei campi DatiMacchine + StatoMacchine per l'impianto indicato + /// + /// + /// + public async Task> mDatiMacchineAsync(string idxMacchina) + { + // hard coded dimensione vettore DatiMacchine + Dictionary answ = new Dictionary(); + // ORA recupero da memoria redis... + try + { + var currHash = 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 dei TASK (da passare a IOB-WIN) per l'impianto indicato + /// + /// + /// + public Dictionary mOptParMacchina(string idxMacchina) + { + // hard coded dimensione vettore DatiMacchine + Dictionary answ = new Dictionary(); + // ORA recupero da memoria redis... + RedisKey currHash = Utils.RedKeyOptPar(idxMacchina, MpIoNS); + answ = RedisGetHashDict(currHash); + return answ; + } + + /// + /// Restitusice elenco KVP dei TASK SALVATI (da passare a IOB-WIN) per l'impianto indicato + /// + /// + /// + public Dictionary mSavedTaskMacchina(string idxMacchina) + { + // hard coded dimensione vettore DatiMacchine + Dictionary answ = new Dictionary(); + // ORA recupero da memoria redis... + try + { + RedisKey currHash = Utils.RedKeySavedTask2ExeMacc(idxMacchina); + answ = RedisGetHashDict(currHash); + } + catch (Exception exc) + { + Log.Info($"Errore in recupero dati SAVED TASK x Redis mSavedTaskMacchina | idxMacchina {idxMacchina}{Environment.NewLine}{exc}"); + } + return answ; + } + + /// + /// Elenco da tabella MappaStatoExplModel + /// + /// + public async Task> MseGetAllAsync(bool forceDb = false) + { + Stopwatch sw = new Stopwatch(); + string source = "DB"; + sw.Start(); + List? result = new List(); + // cerco in _redisConn... + RedisValue rawData = await redisDb.StringGetAsync(Constants.redisMseKey); + if (rawData.HasValue && !forceDb) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + source = "REDIS"; + } + else + { + result = await IocDbController.MseGetAllAsync(maxAge); + // serializzp e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(Constants.redisMseKey, rawData, getRandTOut(redisShortTimeCache / 2)); + } + if (result == null) + { + result = new List(); + } + sw.Stop(); + Log.Debug($"MseGetAllAsync | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Restitusice elenco KVP (async) per evitare blocchi quando chiamato da metodi asincroni + /// + /// + /// + public async Task[]> mTabMSMIAsync(string idxMacchina) + { + KeyValuePair[] answ = new KeyValuePair[1]; + answ[0] = new KeyValuePair("0", "0"); + try + { + var currHash = 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; + } + + /// + /// Restitusice elenco KVP dei TASK (da passare a IOB-WIN) per l'impianto indicato + /// + /// + /// + public Dictionary mTaskMacchina(string idxMacchina) + { + // hard coded dimensione vettore DatiMacchine + Dictionary answ = new Dictionary(); + // ORA recupero da memoria redis... + try + { + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + answ = RedisGetHashDict(currHash); + } + catch (Exception exc) + { + Log.Error(string.Format("Errore in mTaskMacchina | idxMacchina {2}:{0}{1}", Environment.NewLine, exc, idxMacchina)); + } + return answ; + } + + /// + /// Restitusice elenco KVP dei TASK (da passare a IOB-WIN) per l'impianto indicato + /// + /// + /// + public async Task> mTaskMacchinaAsync(string idxMacchina) + { + // hard coded dimensione vettore DatiMacchine + Dictionary answ = new Dictionary(); + // ORA recupero da memoria redis... + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + answ = await RedisGetHashDictAsync(currHash); + return answ; + } + + /// + /// Generazione autoOdl + /// + /// + /// + /// + /// + /// + public async Task OdlAutoDayGenAsync(string idxMacchina, DateTime dataInizio, DateTime dataFine, string codArticolo) + { + var result = await IocDbController.OdlAutoDayGenAsync(idxMacchina, dataInizio, dataFine, codArticolo); + return result; + } + + /// + /// Generazione autoOdl + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task OdlAutoDayGenFullAsync(string idxMacchina, DateTime dataInizio, DateTime dataFine, string codArticolo, int? pzPODL, int? pzPallet, string? keyRichiesta, int? tcAssegnato, string? codGruppo, bool flgCreaPODL, bool flgCheckTC) + { + var result = await IocDbController.OdlAutoDayGenFullAsync(idxMacchina, dataInizio, dataFine, codArticolo, pzPODL, pzPallet, keyRichiesta, tcAssegnato, codGruppo, flgCreaPODL, flgCheckTC); + return result; + } + + /// + /// Elenco ODL dato batch selezionato + /// + /// Batch richiesto + /// + public async Task> OdlByBatch(string BatchSel) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = Utils.redisOdlByBatch; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.OdlByBatch(BatchSel)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"OdlByBatch | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// ODL da chiave + /// + /// + /// + public ODLExpModel OdlByKey(int IdxOdl) + { + ODLExpModel? result = new ODLExpModel(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + result = SpecDbController.OdlByKey(IdxOdl); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"OdlByKey | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Effettua chiusura dell'ODL indicato, andand + /// + /// idx odl da chiudere + /// idx macchina + /// matricola operatore + /// indica se confermare i pezzi priam di chiudere ODL + public async Task ODLClose(int idxOdl, string idxMacchina, int matrOpr, bool confPezzi) + { + bool fatto = false; + await Task.Delay(1); + // recupero dati x conf modalità conferma + var configData = await ConfigGetAllAsync(); + if (configData != null) + { + bool confRett = false; + var currRec = configData.FirstOrDefault(x => x.Chiave == "confRett"); + if (currRec != null) + { + bool.TryParse(currRec.Valore, out confRett); + } + int modoConfProd = 0; + currRec = configData.FirstOrDefault(x => x.Chiave == "modoConfProd"); + if (currRec != null) + { + int.TryParse(currRec.Valore, out modoConfProd); + } + // chiamo metodo conferma! + fatto = await SpecDbController.ODLClose(idxOdl, idxMacchina, matrOpr, confPezzi, confRett, modoConfProd); + } + + return fatto; + } + + public async Task OdlCurrByMaccAsync(string IdxMacchina) + { + ODLExpModel result = new(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisOdlList}:Current:{IdxMacchina}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject($"{rawData}") ?? new(); + readType = "REDIS"; + } + else + { + result = await IocDbController.OdlCurrByMaccAsync(IdxMacchina); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisShortTimeCache)); + } + if (result == null) + { + result = new(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Trace($"OdlCurrByMaccAsync | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Record ODL da chaive + /// + /// + public async Task OdlGetByKey(int IdxOdl) + { + await Task.Delay(1); + var dbResult = await SpecDbController.OdlGetByKey(IdxOdl); + return dbResult; + } + + /// + /// ODL correnti (tutti) + /// + /// + /// + public List OdlGetCurrent() + { + List? dbResult = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisOdlCurrByMac}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + try + { + dbResult = JsonConvert.DeserializeObject>($"{rawData}"); + } + catch + { } + readType = "REDIS"; + } + else + { + dbResult = SpecDbController.OdlGetCurrent().Select(x => x.IdxMacchina).Distinct().ToList(); + rawData = JsonConvert.SerializeObject(dbResult); + redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(3)); + } + if (dbResult == null) + { + dbResult = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"OdlGetCurrent | Read from {readType}: {ts.TotalMilliseconds}ms"); + + return dbResult; + } + + /// + /// elenco TUTTI gli ODL + /// + /// + /// + public List OdlListAll() + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + result = SpecDbController.OdlListAll(); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"OdlListAll | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Elenco ODL filtrati x stato, articolo, KeyRich (che contiene stato) + /// + /// Stato ODL: true=in corso/completato + /// Cod articolo + /// KeyRich (parziale) da cercare (es cod stato x yacht) + /// Reparto selezionato + /// Macchina selezionata + /// Data inizio + /// Data fine + /// + public async Task> OdlListGetFilt(bool inCorso, string codArt, string keyRichPart, string Reparto, string IdxMacchina, DateTime startDate, DateTime endDate) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisOdlList}:{inCorso}:{codArt}:{keyRichPart}:{Reparto}:{IdxMacchina}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.ListODLFilt(inCorso, codArt, keyRichPart, Reparto, IdxMacchina, startDate, endDate)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisShortTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"OdlListGetFilt | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Elenco di tutti i parametri filtrati x macchina + /// + /// * = tutte, altrimenti solo x una data macchina + /// + public async Task> ParametriGetFilt(string IdxMacchina) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisFluxByMac}:{IdxMacchina}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.ParametriGetFilt(IdxMacchina)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"ParametriGetFilt | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Eliminazione record selezionato + /// + /// + /// + public async Task POdlDeleteRecord(PODLExpModel currRec) + { + var dbResult = await SpecDbController.PODLDeleteRecord(currRec); + // elimino cache redis... + await POdlFlushCache(); + await Task.Delay(1); + return dbResult; + } + + /// + /// Avvio fase setup per il record selezionato + /// + /// + /// + public async Task POdlDoSetup(PODLExpModel currRec) + { + var dbResult = await SpecDbController.PODL_startSetup(currRec, 0, 1, 1, "", DateTime.Now); + // elimino cache redis... + await POdlFlushCache(); + await Task.Delay(1); + return dbResult; + } + + /// + /// Recupero PODL da chiave + /// + /// + /// + public async Task POdlGetByKey(int idxPODL) + { + PODLModel result = new PODLModel(); + if (idxPODL != 0) + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisPOdlByPOdl}:{idxPODL}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + var rawResult = JsonConvert.DeserializeObject($"{rawData}"); + if (rawResult != null) + { + result = rawResult; + readType = "REDIS"; + } + } + else + { + result = await SpecDbController.PODL_getByKey(idxPODL); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new PODLModel(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Trace($"POdlGetByKey | Read from {readType}: {ts.TotalMilliseconds}ms"); + } + else + { + Log.Debug("Errore IdxPODL = 0"); + } + return result; + } + + public async Task> POdlGetByMaccArtAsync(string idxMacchina, string codArticolo, string codGruppo, bool onlyFree) + { + List result = new List(); + + var currKey = $"{Utils.redisPOdlByMaccArt}:{idxMacchina}"; + if (!string.IsNullOrEmpty(codArticolo)) + { + currKey += $":A{codArticolo}"; + } + if (!string.IsNullOrEmpty(codGruppo)) + { + currKey += $":G{codGruppo}"; + } + currKey += onlyFree ? $":FREE" : ":ALL"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + result = await IocDbController.POdlGetByMaccArtAsync(idxMacchina, codArticolo, codGruppo, onlyFree); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + return result; + } + + /// + /// Recupero PODL da IdxODL + /// + /// + /// + public PODLModel POdlGetByOdl(int idxODL) + { + PODLModel result = new PODLModel(); + if (idxODL != 0) + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisPOdlByOdl}:{idxODL}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + var rawResult = JsonConvert.DeserializeObject($"{rawData}"); + if (rawResult != null) + { + result = rawResult; + } + readType = "REDIS"; + } + else + { + result = SpecDbController.PODL_getByOdl(idxODL); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new PODLModel(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Trace($"POdlGetByOdl | Read from {readType}: {ts.TotalMilliseconds}ms"); + } + else + { + Log.Debug("Errore IdxODL = 0"); + } + return result; + } + + /// + /// Elenco PODL non avviati filtrati x articolo, KeyRich (che contiene stato) + /// + /// Solo lanciati (1) o ancora disponibili (0) + /// KeyRich (parziale) da cercare (es cod stato x yacht) + /// Macchina + /// Gruppo + /// Data inizio + /// Data fine + /// + public async Task> POdlListGetFilt(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisPOdlList}:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = await Task.FromResult(SpecDbController.ListPODLFilt(lanciato, keyRichPart, idxMacchina, codGruppo, startDate, endDate)); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisShortTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"POdlListGetFilt | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Aggiornamento record selezionato + /// + /// + /// + public async Task POdlUpdateRecord(PODLModel currRec) + { + var dbResult = await SpecDbController.PODLUpdateRecord(currRec); + // elimino cache redis... + await POdlFlushCache(); + return dbResult; + } + + /// + /// Processa registrazione FL da IOB + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task ProcessFluxLogAsync(string idxMacchina, string flux, string valore, string dtEve, string dtCurr, int contatore, bool disabKA) + { + // se non vietato... + if (!disabKA) + { + // scrivo keep alive!!! (se necessario, altrimenti è in cache...) + await ScriviKeepAliveAsync(idxMacchina, DateTime.Now); + } + + string answ = ""; + DateTime dataOraEvento = GetSrvDtEvent(dtEve, dtCurr); + // inizio processing vero e proprio INPUT... + + if (string.IsNullOrEmpty(idxMacchina) || string.IsNullOrEmpty(valore)) + { + string errore = "processFluxLog | Errore: parametri macchina/valOut vuoti"; + Log.Error(errore); + answ = errore; + } + else + { + FluxLogModel newRec = new FluxLogModel() + { + IdxMacchina = idxMacchina, + dtEvento = dataOraEvento, + CodFlux = flux, + Valore = valore, + Cnt = contatore + }; + await IocDbController.FluxLogInsertAsync(newRec); + // 2022.06.06 salvo su redis il valOut ULTIMO del flux x recupero rapido ultimo valOut + var currKey = Utils.RedKeyLastFLog(idxMacchina, flux, MpIoNS); + // 10 min cache max... + await redisDb.StringSetAsync(currKey, valore, TimeSpan.FromMinutes(10)); + // registro in risposta che è andato tutto bene... + answ = "OK"; + } + + 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; + } + + /// + /// 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; + } + + /// + /// Processa input da IOB eventualmente registrando i segnali inviati + /// + /// + /// + /// + /// + /// + /// + 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; + } + + /// + /// Processa registrazione UserLog da IOB + /// + /// Macchina + /// Flusso: DI/RC/RC + /// valOut = note/valString + /// data evento + /// data corrente + /// contatore invio + /// Matricola Operatore + /// label = causale scarto / tagCode + /// valNum = esitoOk (0/1) / Quantità di scarto associata + /// + public async Task ProcessUserLogAsync(string idxMacchina, string flux, string valore, string dtEve, string dtCurr, int contatore, int matrOpr, string label, int valNum) + { + // scrivo keep alive!!! (se necessario, altrimenti è in cache...) + await ScriviKeepAliveAsync(idxMacchina, DateTime.Now); + // 2017.09.14 trimmo eventualmente lo zero finale dalle date SE supera i millisecondi... + dtEve = dtEve.Length > 17 ? dtEve.Substring(0, 17) : dtEve; + dtCurr = dtCurr.Length > 17 ? dtCurr.Substring(0, 17) : dtCurr; + + string answ = ""; + DateTime dataOraEvento = GetSrvDtEvent(dtEve, dtCurr); + + // inizio processing vero e proprio INPUT... + if (string.IsNullOrEmpty(idxMacchina) || string.IsNullOrEmpty(flux)) + { + string errore = "processFluxLog | Errore: parametri macchina/flux vuoti"; + Log.Error(errore); + answ = errore; + } + else + { + // in base al flusso decido dove e cosa scrivere... + switch (flux) + { + case "DI": + RegistroDichiarazioniModel recDich = new RegistroDichiarazioniModel() + { + IdxMacchina = idxMacchina, + DtRec = dataOraEvento, + MatrOpr = matrOpr, + ValString = valore, + TagCode = label + }; + await IocDbController.RegDichiarInsertAsync(recDich); + break; + + case "RC": + bool esitoOk = valNum != 0; + await IocDbController.RegControlliInsertAsync(idxMacchina, matrOpr, esitoOk, valore, dataOraEvento); + break; + + case "RS": + RegistroScartiModel recSca = new RegistroScartiModel() + { + IdxMacchina = idxMacchina, + DataOra = dataOraEvento, + Causale = label, + Qta = valNum, + Note = valore, + MatrOpr = matrOpr + }; + await IocDbController.RegScartiInsertAsync(recSca); + break; + + default: + break; + } + // registro in risposta che è andato tutto bene... + answ = "OK"; + } + return answ; + } + + /// + /// Restituisce il contapezzi salvato per la macchina + /// + /// + /// + public async Task pzCounter(string idxMacchina) + { + int answ = -1; + try + { + var currKey = Utils.RedKeyPzCount(idxMacchina, MpIoNS); + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + int.TryParse(rawData, out answ); + } + else + { + answ = await PzCounterTcAsync(idxMacchina); + // salvo in _redisConn... + await redisDb.StringSetAsync(currKey, answ.ToString(), TimeSpan.FromSeconds(1)); + } + } + catch (Exception exc) + { + Log.Error($"Eccezione in pzCounter{Environment.NewLine}{exc}"); + } + return answ; + } + + /// + /// Restituisce il contapezzi come CONTEGGIO da TCRilevati per la macchina - ASYNC + /// + /// + /// + 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; + } + + /// + /// Ricerca ricetta su MongoDB dato PODL + /// + /// + /// + public async Task RecipeGetByPODL(int idxPODL) + { + RecipeModel? result = null; + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "MongoDB"; + result = await mongoController.RecipeGetByPODL(idxPODL); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"RecipeGetByPODL | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } + + /// + /// Salva ricetta su MongoDB + /// + /// + /// + public async Task RecipeSetByPODL(RecipeModel currRecord) + { + bool answ = false; + answ = await mongoController.RecipeSetByPODL(currRecord); + if (answ) + { + await POdlFlushCache(); + } + return answ; + } + + /// + /// Effettua conteggio chaivi REDIS dato pattern ricerca + /// + /// + /// + public int RedisCountKey(string keyPattern) + { + int num = 0; + keyPattern = (string.IsNullOrEmpty(keyPattern) ? "**" : keyPattern); + try + { + var listEndpoints = redisConnAdmin.GetEndPoints(); + foreach (var endPoint in listEndpoints) + { + var server = redisConnAdmin.GetServer(endPoint); + foreach (RedisKey item in server.Keys(pattern: keyPattern, database: redisDb.Database, pageSize: 250, cursor: 0L)) + { + num++; + } + } + } + catch (Exception arg) + { + Log.Error($"Eccezione in RedisCountKey{Environment.NewLine}{arg}"); + } + + return num; + } + + /// + /// Esegue eliminazione memoria redis keyVal + /// + /// + /// + public bool RedisDelKey(string keyVal) + { + bool answ = redisDb.KeyDelete((RedisKey)keyVal); +#if false + var listEndpoints = redisConnAdmin.GetEndPoints(); + foreach (var endPoint in listEndpoints) + { + var server = redisConnAdmin.GetServer(endPoint); + if (server != null) + { + redisDb.KeyDelete((RedisKey)keyVal); + answ = true; + } + } +#endif + return answ; + } + + /// + /// Esegue eliminazione memoria redis keyVal + /// + /// + /// + public async Task RedisDelKeyAsync(RedisKey keyVal) + { + return await redisDb.KeyDeleteAsync(keyVal); + } + + /// + /// Esegue flush memoria redis dato keyVal + /// + /// + /// + public bool RedisFlushPattern(string pattern) + { + bool answ = false; + var listEndpoints = redisConnAdmin.GetEndPoints(); + foreach (var endPoint in listEndpoints) + { + var server = redisConnAdmin.GetServer(endPoint); + if (server != null) + { + var keyList = server.Keys(redisDb.Database, pattern); + foreach (var item in keyList) + { + redisDb.KeyDelete(item); + } + answ = true; + } + } + return answ; + } + + /// + /// Esegue flush memoria redis dato keyVal, async + /// + /// + /// + public async Task RedisFlushPatternAsync(RedisValue pattern) + { + Log.Debug($"Richiesta flush pattern: {pattern}"); + + // 1. Target ONLY master (le replica sono in read-only) + var master = redisConnAdmin.GetEndPoints() + .Where(ep => redisConnAdmin.GetServer(ep).IsConnected && !redisConnAdmin.GetServer(ep).IsReplica) + .FirstOrDefault(); + + if (master == null) + { + Log.Warn($"Nessun master Redis raggiungibile per il pattern {pattern}"); + return false; + } + + // 2. Flush intero DB se richiesto + if (pattern.ToString() == "*") + { + Log.Debug($"Full DB reset da pattern {pattern}"); + if (master != null) + { + redisConnAdmin.GetServer(master).FlushDatabase(redisDb.Database); + Log.Info($"Flush database {redisDb.Database} completato"); + } + return true; + } + // altrimenti faccio ciclo! + var server = redisConnAdmin.GetServer(master); + var db = redisConnAdmin.GetDatabase(redisDb.Database); + const int batchSize = 500; + var batch = new List(batchSize); + int deletedCount = 0; + + try + { + // KeysAsync usa SCAN automaticamente quando i risultati sono grandi + await foreach (var key in server.KeysAsync( + database: redisDb.Database, + pattern: pattern.ToString(), + pageSize: batchSize)) + { + batch.Add(key); + if (batch.Count >= batchSize) + { + // Esecuzione batch in parallelo controllato + await Task.WhenAll(batch.Select(k => db.KeyDeleteAsync(k))); + batch.Clear(); + deletedCount += batchSize; + } + } + + // Restanti + if (batch.Count > 0) + { + await Task.WhenAll(batch.Select(k => db.KeyDeleteAsync(k))); + deletedCount += batch.Count; + } + + Log.Info("Flush pattern {Pattern}: eliminate {Count} chiavi", pattern, deletedCount); + return true; + } + catch (RedisConnectionException ex) + { + Log.Error(ex, "Connessione Redis persa durante il flush di {pattern}", pattern); + return false; + } + catch (Exception ex) + { + Log.Error(ex, "Errore imprevisto nel flush pattern {pattern}", pattern); + throw; + } + } + + public KeyValuePair[] RedisGetHash(RedisKey redKey) + { + HashEntry[] rawData = redisDb.HashGetAll(redKey); + var result = rawData.Where(x => !x.Name.IsNull).Select(x => new KeyValuePair($"{x.Name}", $"{x.Value}")).ToArray(); + return result; + } + + public 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; + } + + public Dictionary RedisGetHashDict(RedisKey hashKey) + { + HashEntry[] rawData = redisDb.HashGetAll(hashKey); + var result = rawData.Where(x => !x.Name.IsNull).ToDictionary(x => x.Name.ToString(), x => x.Value.ToString()); + return result; + } + + public 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; + } + + public async Task RedisGetHashFieldAsync(RedisKey key, string hashField) + { + return await redisDb.HashGetAsync(key, hashField); + } + + public bool RedisKeyPresent(RedisKey key) + { + return redisDb.KeyExists(key); + } + + public async Task RedisKeyPresentAsync(RedisKey key) + { + return await redisDb.KeyExistsAsync(key); + } + + public void RedisSetHash(RedisKey redKey, KeyValuePair[] valori, double expireSeconds = -1.0) + { + HashEntry[] redHash = valori.Select(x => new HashEntry(x.Key, x.Value)).ToArray(); + redisDb.HashSet(redKey, redHash); + if (expireSeconds > 0.0) + { + redisDb.KeyExpire(redKey, DateTime.Now.AddSeconds(expireSeconds)); + } + } + + public 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)); + } + } + + public void RedisSetHashDict(RedisKey redKey, Dictionary valori, double expireSeconds = -1.0) + { + HashEntry[] redHash = valori.Select(x => new HashEntry(x.Key, x.Value)).ToArray(); + redisDb.HashSet(redKey, redHash); + if (expireSeconds > 0.0) + { + redisDb.KeyExpire(redKey, DateTime.Now.AddSeconds(expireSeconds)); + } + } + + public async Task RedisSetHashDictAsync(RedisKey redKey, Dictionary 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)); + } + } + + /// + /// Inserisce record RRL + fa pulizia vecchi record + /// + /// + public async Task RemRebootLogAddAsync(RemoteRebootLogModel newRec) + { + // verifica preliminare ultima esecuzione (max 1 ogni 60 min...) + DateTime adesso = DateTime.Now; + bool doClean = false; + if (adesso.Subtract(lastCleanupRRL).TotalMinutes > 60) + { + lastCleanupRRL = adesso; + doClean = true; + } + + string confVal = await tryGetConfigAsync("IO_NumReboot2Keep"); + int num2keep = int.TryParse(confVal, out int n) ? n : 5; + // insert del record + pulizia + bool fatto = await IocDbController.RemRebootLogAddAndCleanAsync(newRec, doClean, num2keep); + if (fatto) + { + // svuota cache + var currKey = $"{Utils.redisRemRebLog}:*"; + await RedisFlushPatternAsync(currKey); + } + return fatto; + } + + /// + /// Recupera tutti i record di RemoteRebootLog + /// + /// + public async Task> RemRebootLogGetAllAsync() + { + // setup parametri costanti + string source = "DB"; + Stopwatch sw = new Stopwatch(); + sw.Start(); + List result = new List(); + // cerco in _redisConn... + var currKey = $"{Utils.redisRemRebLog}:ALL"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + //if (!string.IsNullOrEmpty($"{rawData}")) + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + source = "REDIS"; + } + else + { + result = await IocDbController.RemRebootLogGetAllAsync(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, TimeSpan.FromSeconds(30)); + } + if (result == null) + { + result = new List(); + } + sw.Stop(); + Log.Debug($"RemRebootLogGetAll | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Recupera ultimo record x ogni IdxMacchina x avere ultimo attivo + /// + /// + public async Task> RemRebootLogGetLast() + { + // setup parametri costanti + string source = "DB"; + Stopwatch sw = new Stopwatch(); + sw.Start(); + List result = new List(); + // cerco in _redisConn... + var currKey = $"{Utils.redisRemRebLog}:LAST"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + source = "REDIS"; + } + else + { + result = await IocDbController.RemRebootLogGetLastAsync(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, TimeSpan.FromSeconds(30)); + } + if (result == null) + { + result = new List(); + } + sw.Stop(); + Log.Debug($"RemRebootLogGetLast | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Elimina da elenco KVP il TASK per l'impianto indicato + /// + /// + /// + /// + public async Task> RemTask2ExeMacchinaAsync(string idxMacchina, taskType tName) + { + // hard coded dimensione vettore DatiMacchine + Dictionary answ = new Dictionary(); + // ORA recupero da memoria redis... + try + { + var currHash = Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + answ = await RedisGetHashDictAsync(currHash); + + answ.Remove($"{tName}"); + // riscrivo! + await RedisDelKeyAsync(currHash); + await RedisSetHashDictAsync(currHash, answ); + Log.Info($"Task REM - idxMacchina: {idxMacchina} | taskKey: {tName.ToString()}"); + } + catch (Exception exc) + { + Log.Info(string.Format("Errore in RemTask2ExeMacchinaAsync | idxMacchina {2}:{0}{1}", Environment.NewLine, exc, idxMacchina)); + } + return answ; + } + + /// + /// Resetta (rileggendo) i dati della State Machine multi ingressi nel formato + /// currKey: IdxMacchina + /// value: IdxFamigliaIngresso + /// + /// + /// + public async Task[]> resetMSMIAsync(string idxMacchina) + { + var currHash = Utils.RedKeyMsmi(idxMacchina); + // recupero records + var tabMSMI = await IocDbController.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; + } + + /// + /// Processa registrazione EVENTO CONTEGGIO PEZZI x una data macchina IOB + /// + /// Macchina + /// Pezzi da registrare + /// + public async Task saveCaricoPezzi(string idxMacchina, string qty) + { + // default: 0, non registrato x cautela... + string answ = "0"; + // controllo per proseguire + if (string.IsNullOrEmpty(idxMacchina) || string.IsNullOrEmpty(qty)) + { + string errore = $"Errore: mancano parametri macchina/incremento: idxMacchina {idxMacchina} | qty {qty}"; + Log.Error(errore); + answ = errore; + } + else + { + int numPzIncr = -1; + int.TryParse(qty, out numPzIncr); + // se il conteggio è >= 0 SALVO evento... + if (numPzIncr >= 0) + { + // recupero info tra cui ODL corrente + Dictionary datiMacc = await mDatiMacchineAsync(idxMacchina); + // registro evento 120 --> contapezzi in blocco !!!HARD CODED!!! !!!FIXME!!! + int idxEvento = 120; + DateTime adesso = DateTime.Now; + string codArticolo = "ND"; + if (datiMacc.ContainsKey("CodArticolo")) + { + codArticolo = datiMacc["CodArticolo"]; + } + + // creo evento + EventListModel newRecEv = new EventListModel() + { + CodArticolo = codArticolo, + IdxMacchina = idxMacchina, + IdxTipo = idxEvento, + InizioStato = adesso, + MatrOpr = 0, + pallet = "-", + Value = qty + }; + // salva e processa + var resp = await scriviRigaEventoAsync(newRecEv); + // registro in risposta che è andato tutto bene... ovvero la qty richiesta... + answ = qty; + } + } + return answ; + } + + /// + /// Processa registrazione EVENTO CONTEGGIO PEZZI x una data macchina IOB + /// + /// Macchina + /// Pezzi da registrare + /// DataOra evento + /// DataOra corrente + /// + public async Task SaveCaricoPezziAsync(string idxMacchina, string qty, string dtEve = "", string dtCurr = "") + { + // default: 0, non registrato x cautela... + string answ = "0"; + // Verifica se evento realtime oppure ho data specificata x processing @dtEve + DateTime adesso = DateTime.Now; + DateTime dtEvent = adesso; + bool rtimeProc = string.IsNullOrEmpty(dtEve); + if (!rtimeProc) + { + dtEvent = GetSrvDtEvent(dtEve, dtCurr); + } + // controllo per proseguire + if (!string.IsNullOrEmpty(idxMacchina) && !string.IsNullOrEmpty(qty)) + { + int numPzIncr = -1; + int.TryParse(qty, out numPzIncr); + // se il conteggio è >= 0 SALVO evento... + if (numPzIncr >= 0) + { + var listOdl = await IocDbController.OdlListByMaccPeriodoAsync(idxMacchina, dtEvent, dtEvent.AddSeconds(1)); + if (listOdl != null && listOdl.Count > 0) + { + string codArticolo = listOdl.FirstOrDefault()?.CodArticolo ?? "ND"; + // registro evento 120 --> contapezzi in blocco !!!HARD CODED!!! !!!FIXME!!! + int idxEvento = 120; + // creo evento + EventListModel newRecEv = new EventListModel() + { + CodArticolo = codArticolo, + IdxMacchina = idxMacchina, + IdxTipo = idxEvento, + InizioStato = dtEvent, + MatrOpr = 0, + pallet = "-", + Value = qty + }; + // salva e processa + var resp = await scriviRigaEventoAsync(newRecEv); + // registro in risposta che è andato tutto bene... ovvero la qty richiesta... + answ = qty; + } + } + } + else + { + string errore = $"Errore: mancano parametri macchina/incremento: idxMacchina {idxMacchina} | qty {qty}"; + Log.Error(errore); + answ = errore; + } + return answ; + } + + /// + /// Processa registrazione di un counter x una data macchina IOB + /// + /// + /// contapezzi + /// + 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.Info(errore); + answ = errore; + } + else + { + if (string.IsNullOrEmpty(counter)) + { + string errore = "Errore: parametro counter vuoto"; + Log.Error(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 = 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 SaveDataItemsAsync(string id, List dataList) + { + bool answ = false; + if (mongoController != null) + { + answ = mongoController.SaveMachineDataItems(id, dataList); + } + else + { + // modalità con SqlSb (no mongo) + if (useFactory) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var mtcService = scope.ServiceProvider.GetRequiredService(); + answ = await mtcService.ReplaceMachineDataAsync(id, dataList); + } + else + { + answ = await MtcService.ReplaceMachineDataAsync(id, dataList); + } + } + + return answ; + } + + public async Task SaveMachine2Iob(string idxMacchina, string serData) + { + bool fatto = false; + var currKey = Utils.RedKeyMach2Iob(idxMacchina); + // se != null --> salvo! + if (!string.IsNullOrEmpty(serData)) + { + fatto = await redisDb.StringSetAsync(currKey, serData); + } + return fatto; + } + + public async Task SaveMachineIobConf(string idxMacchina, Dictionary currDict) + { + bool fatto = false; + var currKey = Utils.RedKeyMachIobConf(idxMacchina); + // se != null --> salvo! + if (currDict != null) + { + await RedisSetHashDictAsync(currKey, currDict); + fatto = true; + } + return fatto; + } + + /// + /// salva il segnale di "microstato" (segnale) ASYNC + /// + /// idx macchina + /// valOut ingresso + /// data-ora evento (server) + /// contatore sequenza dati inviati + /// + public 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 IocDbController.SignalLogInsertAsync(newRec); + } + + /// + /// scrive un evento di keepalive sulla tabella + /// + /// + /// + /// + public async Task ScriviKeepAliveAsync(string IdxMacchina, DateTime oraMacchina) + { + // cerco se ho keep alive in redis, + var currKey = Utils.RedKeyHash($"KeepAlive:{IdxMacchina}"); + bool keyPresent = await RedisKeyPresentAsync(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 IocDbController.KeepAliveUpsertAsync(IdxMacchina, DateTime.Now, oraMacchina); + } + } + + /// + /// Salvataggio YAML completo di configurazione dell'IOB + /// + /// + /// + /// + public async Task SetIobConfYamlAsync(string idxMacchina, string iobConfFull) + { + bool answ = false; + // se ho un area memoria valida... + if (!string.IsNullOrEmpty(iobConfFull)) + { + // salvo! + var currKey = Utils.RedKeyIobConfYaml(idxMacchina, MpIoNS); + answ = await redisDb.StringSetAsync(currKey, iobConfFull); + } + return answ; + } + + public async Task SetIobMemMap(string idxMacchina, PlcMemMapDto currMap) + { + bool answ = false; + // se ho un area memoria valida... + if (currMap != null) + { + // salvo! + var currKey = Utils.RedKeyIobMemMap(idxMacchina, MpIoNS); + string serVal = JsonConvert.SerializeObject(currMap); + answ = await redisDb.StringSetAsync(currKey, serVal); + } + 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 + /// + /// + /// + public 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 = 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; + } + + /// + /// Statistiche ODL calcolate (da stored stp_STAT_ODL) + /// + /// + public Task> StatOdl(int IdxOdl) + { + return SpecDbController.OdlStart(IdxOdl); + } + + /// + /// restituisce il valOut da REDIS associato al tag richeisto + /// + /// Chiave in cui cercare il valOut + /// + public string TagConfGetKey(string redKey) + { + string outVal = ""; + // cerco in REDIS la conf x l'IOB + var rawData = redisDb.StringGet(redKey); + if (!string.IsNullOrEmpty(rawData)) + { + outVal = $"{rawData}"; + } + return outVal; + } + + /// + /// Elenco setup dei tag conf correnti + /// + /// + public Task>> TagsGetAll() + { + return Task.FromResult(currTagConf); + } + + /// + /// Esegue traduzione dato vocabolario da Lingua + Lemma + /// + /// + /// + /// + public string Traduci(string lemma, string lingua) + { + string answ = $"[{lemma}]"; + // verifico se ho qualcosa nell'obj vocabolario... + if (ObjVocabolario == null || ObjVocabolario.Count == 0) + { + // inizializzo il vocabolario... + ObjVocabolario = VocabolarioGetAll(); + } + var record = ObjVocabolario.Where(x => x.Lingua == lingua && x.Lemma == lemma).FirstOrDefault(); + if (record != null) + { + answ = record.Traduzione; + } + return answ; + } + + public async Task updateDossierValue(DossierModel currDoss, FluxLogDTO editFL) + { + bool answ = false; + // recupero intero set valori dossier deserializzando... + var fluxLogList = FluxLogDtoGetByFlux(currDoss.Valore); + await Task.Delay(1); + + // se tutto ok + if (fluxLogList != null) + { + // da provare...!!!! + + // elimino vecchio record + var currRec = fluxLogList.FirstOrDefault(x => x.CodFlux == editFL.CodFlux && x.dtEvento == editFL.dtEvento); + if (currRec != null) + { + fluxLogList.Remove(currRec); + // aggiungo nuovo + fluxLogList.Add(editFL); + } + + // serializzo nuovamente valOut + DossierFluxLogDTO? result = new DossierFluxLogDTO(); + var ODLflux = result.ODL.ToList(); + foreach (var item in fluxLogList) + { + ODLflux.Add(item); + } + + DossierFluxLogDTO updatedResult = new DossierFluxLogDTO() { ODL = ODLflux }; + + string rawVal = JsonConvert.SerializeObject(updatedResult); + currDoss.Valore = rawVal; + // aggiorno record sul DB + await SpecDbController.DossiersUpdateValore(currDoss); + } + + return answ; + } + + /// + /// Effettua UPSERT elenco parametri correnti x IOB (se c'è UPDATE, se manca ADD) + /// + /// + /// + /// + public async Task UpsertCurrObjItemsAsync(string idxMacchina, List innovations) + { + bool answ = false; + if (innovations != null) + { + Log.Info($"upsertCurrObjItems | idxMacchina: {idxMacchina} | {innovations.Count} innovations"); + // leggo i valori attuali... + List actValues = MachineParamList(idxMacchina); + // per ogni valOut passatomi faccio insert o update rispetto elenco valori correnti in REDIS + foreach (var item in actValues) + { + // cerco nelle innovazioni SE CI SIA il valOut... + var trovato = innovations.Find(obj => obj.uid == item.uid); + // se non trovato nelle innovazioni... + if (trovato == null) + { + // lo ri-aggiungo x non perderlo + innovations.Add(item); + Log.Trace($"innovations | add | item.uid: {item.uid} | item.value: {item.value}"); + } + else + // altrimenti aggiorno campo (non trasmesso) name e tengo il resto... + { + trovato.name = item.name; + Log.Info($"innovations | update | item.uid: {item.uid} | item.value: {item.value} --> {trovato.value} "); + } + } + // serializzo e salvo + string serVal = JsonConvert.SerializeObject(innovations); + + var currKey = Utils.RedKeyCurrObjItems(idxMacchina, MpIoNS); + RedisValue rawData = redisDb.StringSet(currKey, serVal); + } + return answ; + } + + /// + /// Restituisce il valore SPECIFICATO per la state machine ingressi + /// value: iTipoEv_nState (IdxTipoEv da trasmettere + New MICRO-STATE) + /// + /// + /// + /// + /// + public async Task ValoreSmiAsync(int idxFamIn, int idxMicroStato, int valoreIn) + { + string valOut = ""; + var currHash = Utils.GetHashSMI(idxFamIn); + string field = $"{idxMicroStato}_{valoreIn}"; + var searchVal = await RedisGetHashFieldAsync(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; + } + + /// + /// Elenco completo tabella Vocabolario + /// + /// + public List VocabolarioGetAll() + { + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + List? result = new List(); + string source = "REDIS"; + // cerco in redis... + RedisValue rawData = redisDb.StringGet(Utils.redisVocabolario); + if (!string.IsNullOrEmpty($"{rawData}")) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + } + else + { + result = SpecDbController.VocabolarioGetAll(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(Utils.redisVocabolario, rawData, getRandTOut(redisLongTimeCache / 5)); + source = "DB"; + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"VocabolarioGetAll Read from {source}: {ts.TotalMilliseconds}ms"); + if (result == null) + { + result = new List(); + } + return result; + } + + #endregion Public Methods + + #region Internal Methods + + /// + /// Recupera ArtNum dato CodArt (per impianti che accettano solo INT in scrittura) + /// + /// + /// CodArt richiesto, se vuoto restituisce TUTTI i valori in tabella di decodifica + /// + /// Dizionario contenente le righe delle codifiche attive Articolo/Numero + internal async Task> GetArtNumAsync(string codArt) + { + Dictionary answ = new Dictionary(); + var currData = await DecNumArtGetFiltAsync(codArt); + foreach (var item in currData) + { + answ.Add(item.CodArticolo, item.NumART); + } + return answ; + } + + #endregion Internal Methods + + #region Protected Fields + + protected Random rand = new Random(); + + #endregion Protected Fields + + #region Protected Properties + + protected string canCacheParametri { get; set; } = ""; + + #endregion Protected Properties + + #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 IConfiguration _configuration = null!; + private static ILogger _logger = null!; + private static Logger Log = LogManager.GetCurrentClassLogger(); + private static IMtcSetupService MtcService = null!; +#if false + /// + /// Elenco completo valori Macchine 2 Slave + /// + /// + public List Macchine2SlaveGetAll() + { + List? result = new List(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + string readType = "DB"; + string currKey = $"{Utils.redisBaseAddr}:M2STab"; + // cerco in redis dato valore sel macchina... + RedisValue rawData = redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + readType = "REDIS"; + } + else + { + result = IocDbController.Macchine2Slave(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (result == null) + { + result = new List(); + } + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + Log.Debug($"Macchine2SlaveGetAll | Read from {readType}: {ts.TotalMilliseconds}ms"); + return result; + } +#endif + 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"; + + /// + /// Ultima esecuzione pulizia RRL x evitgare congestioni + /// + private DateTime lastCleanupRRL = DateTime.Now.AddHours(-1); + + /// + /// MS max age x dato MSE + /// + private int maxAge = 2000; + + private string MpIoNS = ""; + + /// + /// Oggetto vocabolario x uso continuo traduzione + /// + private List ObjVocabolario = new List(); + + /// + /// Oggetto per connessione a REDIS + /// + private ConnectionMultiplexer redisConn = null!; + + /// + /// Oggetto per connessione a REDIS modalità admin (ex flux dati) + /// + private ConnectionMultiplexer redisConnAdmin = null!; + + /// + /// Oggetto DB redis da impiegare x chiamate R/W + /// + private IDatabase redisDb = null!; + + private int redisLongTimeCache = 5; + + private int redisShortTimeCache = 2; + + /// + /// Generatore random classe + /// + private Random rnd = new Random(); + + private bool useFactory = false; + + #endregion Private Fields + + #region Private Methods + + /// + /// Verifica se sia necessario inserire un cambio di stato impianto (DiarioDi Bordo) in modalità batch + /// + /// + /// + /// + /// + /// + /// + /// + /// + private async Task CheckCambiaStatoBatchAsync(tipoInputEvento tipoInput, string IdxMacchina, DateTime InizioStato, int IdxTipo, string CodArt, string Value, int MatrOpr, string pallet) + { + await IocDbController.CheckCambiaStatoBatchAsync(tipoInput, IdxMacchina, InizioStato, IdxTipo, CodArt, Value, MatrOpr, pallet); +#if false + List listTransit = new List(); + TransizioneStatiModel? rigaTrans = null; + switch (tipoInput) + { + case tipoInputEvento.barcode: + // effettuo cambio stato INDIPENDENTEMENTE da stato precedente + listTransit = await IocDbController.SMES_getUserForcedAsync(IdxMacchina, IdxTipo); + + if (listTransit.Count > 0) + { + rigaTrans = listTransit.FirstOrDefault(); + // solo se cambia stato... + if (rigaTrans != null && rigaTrans.IdxStato != rigaTrans.next_IdxStato) + { + await IocDbController.DDB_InsStatoBatchAsync(IdxMacchina, InizioStato, rigaTrans.next_IdxStato, CodArt, Value, MatrOpr, pallet); + // aggiorno MSE + await IocDbController.RecalcMseAsync(IdxMacchina, 0); + } + } + else + { + Log.Debug($"Non trovata riga per: BARCODE | IdxMacchina: {IdxMacchina} | IdxTipo: {IdxTipo} | CodArt: {CodArt} | Value: {Value} | MatrOpr: {MatrOpr} | pallet: {pallet}"); + } + break; + + case tipoInputEvento.hw: + // verifico se ci sia necessità di cambio stato + listTransit = await IocDbController.SMES_getHwTransitionsAsync(IdxMacchina, IdxTipo); + if (listTransit.Count > 0) + { + rigaTrans = listTransit.FirstOrDefault(); + if (rigaTrans != null && rigaTrans.IdxStato != rigaTrans.next_IdxStato) + { + await IocDbController.DDB_InsStatoBatchAsync(IdxMacchina, InizioStato, rigaTrans.next_IdxStato, CodArt, Value, MatrOpr, pallet); + // aggiorno MSE + await IocDbController.RecalcMseAsync(IdxMacchina, 0); + } + } + else + { + Log.Debug($"Non trovata riga per: HW | IdxMacchina: {IdxMacchina} | IdxTipo: {IdxTipo} | CodArt: {CodArt} | Value: {Value} | MatrOpr: {MatrOpr} | pallet: {pallet}"); + } + break; + + default: + break; + } +#endif + } + + /// + /// Restituisce l'elenco codici flusso (da confFlux) x una macchina (se presenti) Impiegata + /// anche cache redis + /// + /// + /// + private async Task> ConfFluxMach(string idxMacchina) + { + List resultList = new List(); + string tag = string.IsNullOrEmpty(idxMacchina) ? "ALL" : idxMacchina; + var currKey = $"{Utils.redisConfFlux}:{tag}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + resultList = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + var dbData = await IocDbController.ConfFluxFiltAsync(idxMacchina); + resultList = dbData + .Select(x => x.CodFlux) + .ToList(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(resultList); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (resultList == null) + { + resultList = new(); + } + return resultList; + } + + /// + /// Svuota la cache redis x l'elenco delle righe di confFlux x una macchina (se presenti) + /// + /// + /// + private async Task DossierLastByMachResetAsync(string idxMacchina) + { + bool answ = false; + var currKey = $"{Utils.redisDossByMacLast}:{idxMacchina}"; + await redisDb.KeyDeleteAsync(currKey); + 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 l'elenco delle data-ora di confFlux x una macchina (se presenti) Impiegata + /// anche cache redis + /// + /// + /// num record da recuperare + /// + private async Task> FluxLogFirstByMachAsync(string idxMacchina, int numMax = 10) + { + List resultList = new List(); + + var currKey = $"{Utils.redisFluxByMacFirst}:{idxMacchina}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + resultList = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + var dbData = await IocDbController.FluxLogFirstByMaccAsync(idxMacchina, numMax); + resultList = dbData + .Select(x => x.dtEvento) + .ToList(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(resultList); + await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + if (resultList == null) + { + resultList = new List(); + } + return resultList; + } + + /// + /// Effettua vera chiamata x salvataggio snapshot dati FluxLog + /// + /// + /// + /// + /// + private async Task FluxLogSaveSnapshotAsync(string id, DateTime dtStart, DateTime dtEnd, string caller) + { + string answ = ""; + DateTime dataOraEvento = DateTime.Now; + Log.Debug($"{caller} | Richiesta snapshot dati FluxLog macchina: id: {id} | periodo: {dtStart} - {dtEnd}"); + try + { + bool fatto = await IocDbController.FluxLogTakeSnapshotLastAsync(id, dtStart, dtEnd); + answ = fatto ? "OK" : "KO"; + } + catch (Exception exc) + { + Log.Error($"Errore in {caller}{Environment.NewLine}{exc}"); + answ = "NO"; + } + return answ; + } + + /// + /// 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; + } + + /// + /// 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 = $"{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 = $"{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; + } + + private async Task POdlFlushCache() + { + bool answ = false; + RedisValue pattern = new RedisValue($"{Utils.redisXdlData}:*"); + answ = await RedisFlushPatternAsync(pattern); + pattern = new RedisValue($"{Utils.redisPOdlByOdl}:*"); + answ = await RedisFlushPatternAsync(pattern); + pattern = new RedisValue($"{Utils.redisPOdlByPOdl}:*"); + answ = await RedisFlushPatternAsync(pattern); + pattern = new RedisValue($"{Utils.redisPOdlList}:*"); + answ = await RedisFlushPatternAsync(pattern); + + return answ; + } + + /// + /// 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 resetCacheArticoli() + { + RedisValue pattern = new RedisValue($"{Utils.redisArtByDossier}:*"); + await RedisFlushPatternAsync(pattern); + pattern = new RedisValue($"{Utils.redisArtList}:*"); + await RedisFlushPatternAsync(pattern); + } + + /// + /// Restitusice elenco KVP dei campi DatiMacchine + StatoMacchine per l'impianto indicato + /// + /// + /// + private async Task> ResetDatiMacchinaAsync(string idxMacc) + { + Dictionary result = new Dictionary(); + VMSFDModel? dbResult = null; + if (useFactory) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var mtcService = scope.ServiceProvider.GetRequiredService(); + dbResult = await mtcService.VMSFDGetByMaccAsync(idxMacc); + } + else + { + dbResult = await IocDbController.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 = 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 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 = Utils.GetHashSMI(idxFamIn); + // leggo da DB... + var tabSMI = await IocDbController.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; + } + +#if false + /// + /// Scrive una riga di evento nel db + check cambio stato DiarioDiBordo + /// + /// codice macchina + /// + private inputComandoMapo scriviRigaEvento(EventListModel newRec) + { + bool inserito = false; + try + { + // inserisco evento + inserito = IocDbController.EvListInsert(newRec); + // faccio controllo per eventuale cambio stato da tab transizioni... + CheckCambiaStatoBatchAsync(tipoInputEvento.hw, newRec.IdxMacchina, newRec.InizioStato ?? DateTime.Now, newRec.IdxTipo, newRec.CodArticolo, newRec.Value, newRec.MatrOpr, newRec.pallet); + } + catch (Exception exc) + { + Log.Error($"Errore in scriviRigaEvento | IdxMacchina {newRec.IdxMacchina} | IdxTipo {newRec.IdxTipo} | codArticolo {newRec.CodArticolo} | Value {newRec.Value} | MatrOpr {newRec.MatrOpr} | Pallet {newRec.pallet} | dTime {newRec.InizioStato}{Environment.NewLine}{exc}"); + } + // formatto output + inputComandoMapo answ = new inputComandoMapo(); + answ.outValue = inserito.ToString(); + answ.needStatusRefresh = true; + return answ; + } +#endif + + /// + /// Scrive una riga di evento nel db + check cambio stato DiarioDiBordo + /// + /// codice macchina + /// + private async Task scriviRigaEventoAsync(EventListModel newRec) + { + bool inserito = false; + try + { + // inserisco evento + inserito = await IocDbController.EvListInsertAsync(newRec); + // faccio controllo per eventuale cambio stato da tab transizioni... + await CheckCambiaStatoBatchAsync(tipoInputEvento.hw, newRec.IdxMacchina, newRec.InizioStato ?? DateTime.Now, newRec.IdxTipo, newRec.CodArticolo, newRec.Value, newRec.MatrOpr, newRec.pallet); + } + catch (Exception exc) + { + Log.Error($"Errore in scriviRigaEvento | IdxMacchina {newRec.IdxMacchina} | IdxTipo {newRec.IdxTipo} | codArticolo {newRec.CodArticolo} | Value {newRec.Value} | MatrOpr {newRec.MatrOpr} | Pallet {newRec.pallet} | dTime {newRec.InizioStato}{Environment.NewLine}{exc}"); + } + // formatto output + inputComandoMapo answ = new inputComandoMapo(); + answ.outValue = inserito.ToString(); + answ.needStatusRefresh = true; + return answ; + } + + /// + /// 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; + if (useFactory) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var iocService = scope.ServiceProvider.GetRequiredService(); + + // inserisco evento + inserito = await iocService.EvListMicroStatoInsertAsync(newRecMsm, newRecEv); + // faccio controllo per eventuale cambio stato da tab transizioni... + + await iocService.CheckCambiaStatoBatchAsync(tipoInputEvento.hw, newRecEv.IdxMacchina, newRecEv.InizioStato ?? DateTime.Now, newRecEv.IdxTipo, newRecEv.CodArticolo, newRecEv.Value, newRecEv.MatrOpr, newRecEv.pallet); + } + else + { + + try + { + // inserisco evento + inserito = await IocDbController.EvListMicroStatoInsertAsync(newRecMsm, newRecEv); + // faccio controllo per eventuale cambio stato da tab transizioni... + await CheckCambiaStatoBatchAsync(tipoInputEvento.hw, newRecEv.IdxMacchina, newRecEv.InizioStato ?? DateTime.Now, newRecEv.IdxTipo, newRecEv.CodArticolo, newRecEv.Value, newRecEv.MatrOpr, newRecEv.pallet); + } + catch (Exception exc) + { + Log.Error($"Errore in scriviRigaEvento | IdxMacchina {newRecEv.IdxMacchina} | IdxTipo {newRecEv.IdxTipo} | codArticolo {newRecEv.CodArticolo} | Value {newRecEv.Value} | MatrOpr {newRecEv.MatrOpr} | Pallet {newRecEv.pallet} | dTime {newRecEv.InizioStato}{Environment.NewLine}{exc}"); + } + } + // formatto output + inputComandoMapo answ = new inputComandoMapo(); + answ.outValue = inserito.ToString(); + answ.needStatusRefresh = true; + return answ; + } + + + + /// + /// Scrive una riga di evento manuale (barcode) nel db + check cambio stato DiarioDiBordo + /// + /// codice macchina + /// + private async Task scriviRigaEventoBarcodeAsync(EventListModel newRec) + { + bool inserito = false; + try + { + // inserisco evento + inserito = await IocDbController.EvListInsertAsync(newRec); + // faccio controllo per eventuale cambio stato da tab transizioni... + await CheckCambiaStatoBatchAsync(tipoInputEvento.barcode, newRec.IdxMacchina, newRec.InizioStato ?? DateTime.Now, newRec.IdxTipo, newRec.CodArticolo, newRec.Value, newRec.MatrOpr, newRec.pallet); + } + catch (Exception exc) + { + Log.Error($"Errore in scriviRigaEvento | IdxMacchina {newRec.IdxMacchina} | IdxTipo {newRec.IdxTipo} | codArticolo {newRec.CodArticolo} | Value {newRec.Value} | MatrOpr {newRec.MatrOpr} | Pallet {newRec.pallet} | dTime {newRec.InizioStato}{Environment.NewLine}{exc}"); + } + // formatto output + inputComandoMapo answ = new inputComandoMapo(); + answ.outValue = inserito.ToString(); + answ.needStatusRefresh = true; + return answ; + } + + /// + /// Stato prod macchina (completo) + /// + /// + /// + /// + private async Task StatoProdMacchinaAsync(string idxMacchina, DateTime dtReq, bool forceDb = false) + { + // setup parametri costanti + string source = "DB"; + StatoProdModel? result = new StatoProdModel(); + // cerco in _redisConn... + string currKey = $"{Utils.redisStatoProd}:{idxMacchina}:{dtReq:HHmm}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue && !forceDb) + { + result = JsonConvert.DeserializeObject($"{rawData}"); + source = "REDIS"; + } + else + { + result = await IocDbController.StatoProdMacchinaAsync(idxMacchina, dtReq); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, TimeSpan.FromSeconds(60)); + } + if (result == null) + { + result = new StatoProdModel(); + } + return result; + } + + /// + /// 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; + } + +#if false + /// + /// Restituisce il valOut SPECIFICATO per la state machine ingressi + /// value: iTipoEv_nState (IdxTipoEv da trasmettere + New MICRO-STATE) + /// + /// + /// + /// + /// + private string valoreSMI(int idxFamIn, int idxMicroStato, int valoreIn) + { + var currHash = Utils.GetHashSMI(idxFamIn); + string field = string.Format("{0}_{1}", idxMicroStato, valoreIn); + return RedisGetHashFieldAsync(currHash, field); + } +#endif + + /// + /// cerca codice in anagrafica macchine ed eventualmente inserisce nuova macchina + /// + /// + private async Task verificaIdxMacchinaAsync(string IdxMacchina) + { + bool needDB = false; + try + { + // esecuzione in REDIS...cerco status macchina... + if ((await mDatiMacchineAsync(IdxMacchina)).Count == 0) + { + needDB = true; + } + } + catch { } + + if (needDB) + { + // verifico se esiste su DB + var dbRec = await IocDbController.MacchineGetByIdxAsync(IdxMacchina); + if (dbRec == null) + { + MacchineModel newRec = new MacchineModel() + { + IdxMacchina = IdxMacchina, + CodMacchina = "0000", + Nome = IdxMacchina, + Descrizione = "Macchina non codificata", + Note = "-", + locazione = "", + RecipeArchivePath = "", + RecipePath = "" + }; + await IocDbController.MacchineUpsertAsync(newRec); + + // verifico ci sia un microstato macchina... + var recMSM = await IocDbController.MicroStatoMacchinaGetByIdxMaccAsync(IdxMacchina); + if (recMSM.Count == 0) + { + // inserisco nuovo stato... + MicroStatoMacchinaModel msRec = new MicroStatoMacchinaModel() + { + IdxMacchina = IdxMacchina, + IdxMicroStato = 0, + InizioStato = DateTime.Now, + Value = "00" + }; + await IocDbController.MicroStatoMacchinaUpsertAsync(msRec); + } + } + } + } + + #endregion Private Methods + } +} diff --git a/MP-RIOC/MP-RIOC.http b/MP-RIOC/MP-RIOC.http new file mode 100644 index 00000000..af71d627 --- /dev/null +++ b/MP-RIOC/MP-RIOC.http @@ -0,0 +1,6 @@ +@MP_RIOC_HostAddress = http://localhost:5290 + +GET {{MP_RIOC_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/MP-RIOC/MP-RIOC.slnx b/MP-RIOC/MP-RIOC.slnx new file mode 100644 index 00000000..455ea819 --- /dev/null +++ b/MP-RIOC/MP-RIOC.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/MP-RIOC/MP.RIOC.csproj b/MP-RIOC/MP.RIOC.csproj new file mode 100644 index 00000000..0c63a52d --- /dev/null +++ b/MP-RIOC/MP.RIOC.csproj @@ -0,0 +1,41 @@ + + + + net8.0 + enable + enable + MP_RIOC + 8.16.2605.808 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MP-RIOC/Pages/Index.cshtml b/MP-RIOC/Pages/Index.cshtml new file mode 100644 index 00000000..48b156b7 --- /dev/null +++ b/MP-RIOC/Pages/Index.cshtml @@ -0,0 +1,25 @@ +@page +@{ + Layout = null; +} + + + + MP.RIOC - Router Status + + + +
+

MP.RIOC

+ MAPO Server Router +

Stato del Servizio: ONLINE

+
+

Questo server smista il traffico tra MP.IO (API Legacy dotNet 4.7.2) e MP.IOC (nuove API .NET 8)

+ Versione Router: @System.Reflection.Assembly.GetExecutingAssembly().GetName().Version +
+ + \ No newline at end of file diff --git a/MP-RIOC/Pages/Index.cshtml.cs b/MP-RIOC/Pages/Index.cshtml.cs new file mode 100644 index 00000000..0d5b03ed --- /dev/null +++ b/MP-RIOC/Pages/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace MP.RIOC.Pages +{ + public class IndexModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/MP-RIOC/Program.cs b/MP-RIOC/Program.cs new file mode 100644 index 00000000..acbc5ca6 --- /dev/null +++ b/MP-RIOC/Program.cs @@ -0,0 +1,131 @@ +using MP.RIOC.Services; +using NLog; +using NLog.Web; +using StackExchange.Redis; +using System.Diagnostics; +using System.Net; +using System.Reflection; + +var builder = WebApplication.CreateBuilder(args); + +// recupero env corrente +var env = builder.Environment; +var logger = LogManager.Setup() + .LoadConfigurationFromAppSettings() + .GetCurrentClassLogger(); + +var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(); +logger.Info($"MP.RIOC | Program.cs: startup | v.{assemblyVersion}"); +logger.Info($"Current ASPNETCORE_ENVIRONMENT: {env.EnvironmentName}"); + +// Config setup +ConfigurationManager configuration = builder.Configuration; +// REDIS setup +logger.Info("Config OK"); +string confRedis = configuration.GetConnectionString("Redis"); +string redisSrvAddr = confRedis.Substring(0, confRedis.IndexOf(":")); +logger.Info("Setup REDIS OK"); + +// 1. Configurazione dell'invoker personalizzato (Risolve i tuoi errori) +var httpClientInvoker = new HttpMessageInvoker(new SocketsHttpHandler +{ + UseProxy = false, + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.None, + UseCookies = false, + // Correzione per il tracing: usa il propagatore corrente di sistema + ActivityHeadersPropagator = DistributedContextPropagator.Current, + ConnectTimeout = TimeSpan.FromSeconds(15), + + // Gestione certificato (ignora errori per localhost/test) + SslOptions = new System.Net.Security.SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true + } +}); + +builder.Services.AddSingleton(httpClientInvoker); +builder.Services.AddHttpForwarder(); + +// avvio oggetto shared x redis... +var redisMux = ConnectionMultiplexer.Connect(confRedis); +builder.Services.AddSingleton(redisMux); + +// Registrazione dei servizi custom +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +logger.Info("Standard service configured"); + +// WeightProvider: Redis/Memory da config +var weightOnRedis = builder.Configuration.GetValue("ServerConf:RedisWeight", false); +if (weightOnRedis) +{ + builder.Services.AddSingleton(); +} +else +{ + builder.Services.AddSingleton(); +} +logger.Info($"Weight service configured | use Redis: {weightOnRedis}"); + +// RouteManager registration (singleton) +builder.Services.AddSingleton(); +logger.Info("Singleton Route Manager registered"); + +// aggiunta pagina razor di stato +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// 1. Configurazione Base Path +string baseUrl = configuration.GetValue("ServerConf:BaseUrlIoc") ?? "/MP/RIOC"; +app.UsePathBase(baseUrl); + +// 2. Middleware statici (essenziali per CSS/JS delle pagine Razor) +app.UseStaticFiles(); + +// 3. Abilita il Routing (necessario per MapGet e MapRazorPages) +app.UseRouting(); + +// 4. Logging middleware +app.Use(async (ctx, next) => +{ + logger.Debug($"Incoming request PathBase='{ctx.Request.PathBase}' Path='{ctx.Request.Path}'"); + await next(); +}); + +// 5. Il cuore del Proxy (MapWhen è terminale per le richieste che lo soddisfano) +string routePath = configuration.GetValue("ServerConf:RoutePath") ?? "/api/IOB"; +string fullPath = $"{baseUrl}{routePath}".Replace("//", "/"); +logger.Info($"BaseUrl: {baseUrl}"); +app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments(routePath, StringComparison.OrdinalIgnoreCase), + builder => + { + builder.Run(async ctx => + { + var routeManager = ctx.RequestServices.GetRequiredService(); + await routeManager.HandleAsync(ctx); + }); + }); + +// 6. Definizione degli Endpoints locali +app.MapRazorPages(); + +app.MapGet("/router-status", () => Results.Ok(new +{ + Status = "Online", + Version = assemblyVersion, + Mode = weightOnRedis ? "Redis" : "InMemory", + Time = DateTime.Now +})); + +// 7. Fallback "intelligente" +// Invece di app.Run, usiamo MapFallback che viene eseguito SOLO se nessun altro endpoint o MapWhen ha risposto +app.MapFallback(async context => +{ + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Router: Endpoint non trovato o non mappato."); +}); + +app.Run(); \ No newline at end of file diff --git a/MP-RIOC/Properties/launchSettings.json b/MP-RIOC/Properties/launchSettings.json new file mode 100644 index 00000000..b57a323d --- /dev/null +++ b/MP-RIOC/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:15280", + "sslPort": 44323 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "MP/RIOC/api/IOB/", + "applicationUrl": "http://localhost:5290", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "MP/RIOC/api/IOB/", + "applicationUrl": "https://localhost:7120;http://localhost:5290", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "MP/RIOC/api/IOB/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MP-RIOC/Services/IWeightProvider.cs b/MP-RIOC/Services/IWeightProvider.cs new file mode 100644 index 00000000..b18e4fcb --- /dev/null +++ b/MP-RIOC/Services/IWeightProvider.cs @@ -0,0 +1,29 @@ +using MP.Core.DTO; + +namespace MP.RIOC.Services +{ + public interface IWeightProvider + { + #region Public Methods + + /// + /// Ritorna l'intero elenco dei weight attivi nel formato WeightDTO + /// + /// + Task> GetAllWeightsAsync(); + + /// + /// Ritorna la coppia (oldWeight, newWeight) per scegliere dove instradare il metodo tra i 2 sistemi API. + /// + (int oldWeight, int newWeight) GetWeightsFor(string method); + + /// + /// Aggiorna/Aggiuinge il valore del weight richiesto + /// + /// + /// + bool UpsertWeight(WeightDTO updRecord); + + #endregion Public Methods + } +} diff --git a/MP-RIOC/Services/InMemoryWeightProvider.cs b/MP-RIOC/Services/InMemoryWeightProvider.cs new file mode 100644 index 00000000..f4baf415 --- /dev/null +++ b/MP-RIOC/Services/InMemoryWeightProvider.cs @@ -0,0 +1,56 @@ +using MP.Core.DTO; +using System.Collections.Concurrent; + +namespace MP.RIOC.Services +{ + public class InMemoryWeightProvider : IWeightProvider + { + private readonly ConcurrentDictionary _map = new(); + private readonly int _defaultOld; + private readonly int _defaultNew; + + public InMemoryWeightProvider(IConfiguration config) + { + _defaultOld = config.GetValue("RouteMan:DefaultWeightOld", 100); + _defaultNew = config.GetValue("RouteMan:DefaultWeightNew", 0); + } + + public (int oldWeight, int newWeight) GetWeightsFor(string method) + { + if (string.IsNullOrEmpty(method)) method = "unknown"; + return _map.GetOrAdd(method, _ => (_defaultOld, _defaultNew)); + } + + public void SetWeights(string method, int oldWeight, int newWeight) + { + _map[method] = (Math.Clamp(oldWeight, 0, 100), Math.Clamp(newWeight, 0, 100)); + } + + public async Task> GetAllWeightsAsync() + { + var result = new List(); + await Task.Delay(1); + + foreach (var kvp in _map) + { + result.Add(new WeightDTO + { + Method = kvp.Key, + OldWeight = Math.Clamp(kvp.Value.oldW, 0, 100), + NewWeight = Math.Clamp(kvp.Value.newW, 0, 100) + }); + } + return result; + } + + public bool UpsertWeight(WeightDTO updRecord) + { + if (updRecord == null || string.IsNullOrEmpty(updRecord.Method)) + return false; + + _map[updRecord.Method] = (Math.Clamp(updRecord.OldWeight, 0, 100), Math.Clamp(updRecord.NewWeight, 0, 100)); + + return true; + } + } +} diff --git a/MP-RIOC/Services/PreserveBodyTransformer.cs b/MP-RIOC/Services/PreserveBodyTransformer.cs new file mode 100644 index 00000000..2434f658 --- /dev/null +++ b/MP-RIOC/Services/PreserveBodyTransformer.cs @@ -0,0 +1,38 @@ +using Yarp.ReverseProxy.Forwarder; + +namespace MP.RIOC.Services +{ + public class PreserveBodyTransformer : HttpTransformer + { + public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) + { + // Chiama il base (usa overload con cancellationToken) + await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); + + // Imposta il method correttamente + proxyRequest.Method = new HttpMethod(httpContext.Request.Method); + + // Se vuoi leggere/loggare il body, abilita buffering e rewind. + // NON assegnare proxyRequest.Content: YARP copierà il body da HttpContext.Request. + if (httpContext.Request.ContentLength > 0 || httpContext.Request.Body.CanRead) + { + // Abilita buffering solo se necessario (attenzione a payload grandi) + httpContext.Request.EnableBuffering(); + + // Rewind per sicurezza + httpContext.Request.Body.Position = 0; + + // Se vuoi leggere il body per logging, fallo qui ma non sostituire proxyRequest.Content. + // Esempio (opzionale): leggere senza consumare + // using var sr = new StreamReader(httpContext.Request.Body, leaveOpen: true); + // var bodyText = await sr.ReadToEndAsync(); + // httpContext.Request.Body.Position = 0; + } + } + + public override ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse, CancellationToken cancellationToken) + { + return base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken); + } + } +} diff --git a/MP-RIOC/Services/RedisWeightProvider.cs b/MP-RIOC/Services/RedisWeightProvider.cs new file mode 100644 index 00000000..e003c8e5 --- /dev/null +++ b/MP-RIOC/Services/RedisWeightProvider.cs @@ -0,0 +1,157 @@ +using MP.Core.DTO; +using StackExchange.Redis; + +namespace MP.RIOC.Services +{ + public class RedisWeightProvider : IWeightProvider + { + #region Public Constructors + + public RedisWeightProvider(IConnectionMultiplexer mux, IConfiguration config) + { + _config = config; + _db = mux.GetDatabase(); + _mux = mux; + _defaultOld = config.GetValue("RouteMan:DefaultWeightOld", 100); + _defaultNew = config.GetValue("RouteMan:DefaultWeightNew", 0); + _redisBaseKey = config.GetValue("ServerConf:RedisBaseKey") ?? "MP_IOC"; + _keyPrefix = $"{_redisBaseKey}:route_weight:"; + } + + #endregion Public Constructors + + #region Public Methods + + /// + /// Ritorna (oldWeight, newWeight) per il metodo. Se non esiste, crea la chiave con i default. + /// + public (int oldWeight, int newWeight) GetWeightsFor(string method) + { + if (string.IsNullOrEmpty(method)) method = "unknown"; + var key = _keyPrefix + method; + + // Leggi entrambi i campi + var oldVal = _db.HashGet(key, "old"); + var newVal = _db.HashGet(key, "new"); + + // Se entrambi mancanti, inizializza con default (usando HSet con When.NotExists per evitare overwrite) + if (oldVal.IsNull && newVal.IsNull) + { + // Imposta i campi singolarmente con When.NotExists per evitare overwrite + _db.HashSet(key, "old", _defaultOld, When.NotExists); + _db.HashSet(key, "new", _defaultNew, When.NotExists); + + // Rileggi per essere sicuri + oldVal = _db.HashGet(key, "old"); + newVal = _db.HashGet(key, "new"); + } + + // Se uno dei due manca, impostalo al default (non sovrascrive l'altro) + if (oldVal.IsNull) + { + _db.HashSet(key, "old", _defaultOld, When.NotExists); + oldVal = _defaultOld; + } + if (newVal.IsNull) + { + _db.HashSet(key, "new", _defaultNew, When.NotExists); + newVal = _defaultNew; + } + + if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld; + if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew; + + // clamp 0..100 + oldW = Math.Clamp(oldW, 0, 100); + newW = Math.Clamp(newW, 0, 100); + + return (oldW, newW); + } + + // API per aggiornare i pesi a runtime (opzionale) + public void SetWeights(string method, int oldWeight, int newWeight) + { + var key = _keyPrefix + (string.IsNullOrEmpty(method) ? "unknown" : method); + _db.HashSet(key, new HashEntry[] { + new HashEntry("old", Math.Clamp(oldWeight,0,100)), + new HashEntry("new", Math.Clamp(newWeight,0,100)) + }); + } + + public async Task> GetAllWeightsAsync() + { + var result = new List(); + var server = _mux.GetServer(_mux.GetEndPoints().First()); + + if (server.IsReplica) + { + return result; + } + + await foreach (var key in server.KeysAsync(pattern: $"{_keyPrefix}*")) + { + var methodName = KeyToString(key.ToString()); + if (string.IsNullOrEmpty(methodName)) continue; + + var oldVal = _db.HashGet(key, "old"); + var newVal = _db.HashGet(key, "new"); + + int oldW = 100; + int newW = 0; + + if (!oldVal.IsNull && int.TryParse(oldVal.ToString(), out var parsedOld)) + oldW = Math.Clamp(parsedOld, 0, 100); + + if (!newVal.IsNull && int.TryParse(newVal.ToString(), out var parsedNew)) + newW = Math.Clamp(parsedNew, 0, 100); + + result.Add(new WeightDTO { Method = methodName, OldWeight = oldW, NewWeight = newW }); + } + + // riordino desc x NEW poi alfabetico... + result = result + .OrderByDescending(x => x.NewWeight) + .ThenBy(x => x.Method) + .ToList(); + + return result; + } + + public bool UpsertWeight(WeightDTO updRecord) + { + if (updRecord == null || string.IsNullOrEmpty(updRecord.Method)) + return false; + + var key = _keyPrefix + updRecord.Method; + _db.HashSet(key, new HashEntry[] { + new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)), + new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100)) + }); + + return true; + } + + private string KeyToString(string key) + { + if (string.IsNullOrEmpty(key)) return ""; + var prefix = _keyPrefix ?? ""; + if (key.StartsWith(prefix)) + return key.Substring(prefix.Length); + return key; + } + + #endregion Public Methods + + #region Private Fields + + private static string _keyPrefix = "route_weight:"; + private static string _redisBaseKey = ""; + private readonly IConfiguration _config; + private readonly IDatabase _db; + private readonly IConnectionMultiplexer _mux; + private readonly int _defaultNew; + private readonly int _defaultOld; + + #endregion Private Fields + } +} diff --git a/MP-RIOC/Services/RouteManager.cs b/MP-RIOC/Services/RouteManager.cs new file mode 100644 index 00000000..3febe3c9 --- /dev/null +++ b/MP-RIOC/Services/RouteManager.cs @@ -0,0 +1,167 @@ +using NLog; +using System.Diagnostics; +using Yarp.ReverseProxy.Forwarder; + +namespace MP.RIOC.Services +{ + public class RouteManager + { + private readonly IHttpForwarder _forwarder; + private readonly HttpMessageInvoker _httpClientInvoker; + private readonly PreserveBodyTransformer _transformer; + private readonly RouteStatsManager _stats; + private readonly IWeightProvider _weightProvider; + private readonly IConfiguration _config; + private readonly ForwarderRequestConfig _forwarderConfig; + + public RouteManager( + IHttpForwarder forwarder, + HttpMessageInvoker httpClientInvoker, + PreserveBodyTransformer transformer, + RouteStatsManager stats, + IWeightProvider weightProvider, + IConfiguration config) + { + _forwarder = forwarder; + _httpClientInvoker = httpClientInvoker; + _transformer = transformer; + _stats = stats; + _weightProvider = weightProvider; + _config = config; + _routePath = _config.GetValue("ServerConf:RoutePath") ?? "/api/IOB"; + _forwarderConfig = new ForwarderRequestConfig + { + ActivityTimeout = TimeSpan.FromSeconds(30), + // Parse della versione (es. "1.1") + Version = Version.Parse(_config.GetValue("ServerConf:HttpVersion") ?? "1.1"), + // Policy per la versione + VersionPolicy = _config.GetValue("ServerConf:HttpVersionPolicy") + ?? HttpVersionPolicy.RequestVersionExact + }; + } + + private string _routePath = ""; + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + + public async Task HandleAsync(HttpContext context) + { + var sw = Stopwatch.StartNew(); + var routePrefix = new PathString(_routePath); + var fullPrefix = context.Request.PathBase.Add(routePrefix); + + string relativePath; + string query = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; + + if (context.Request.Path.StartsWithSegments(fullPrefix, out var remaining)) + { + relativePath = remaining.Value.TrimStart('/'); + } + else if (context.Request.Path.StartsWithSegments(routePrefix, out remaining)) + { + relativePath = remaining.Value.TrimStart('/'); + } + else + { + var fullPath = (context.Request.PathBase + context.Request.Path).Value ?? ""; + var idx = fullPath.IndexOf("/RIOB/", StringComparison.OrdinalIgnoreCase); + relativePath = idx >= 0 ? fullPath[(idx + "/RIOB/".Length)..] : fullPath.TrimStart('/'); + } + + Log.Debug($"PathBase={context.Request.PathBase} | Path={context.Request.Path} | relativePath={relativePath}"); + + // Procedo a calcolare metodo e ID... + string metodo = "/"; + string id = "ALL"; + if (!string.IsNullOrEmpty(relativePath)) + { + // Rimuovo eventuale query string + var pathOnly = relativePath.Split('?')[0]; + + // Splitto per / + var parts = pathOnly.Split('/', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > 0) metodo = parts[0]; + if (parts.Length > 1) id = parts[1]; + } + + Log.Debug($"Metodo: {metodo} | machineId: {id}"); + + var (oldW, newW) = _weightProvider.GetWeightsFor(metodo); + var pickNew = DecideByWeights(oldW, newW); + var target = pickNew ? "IOC" : "IO"; + + // Costruisci destination base in base al target + var destBase = pickNew + ? _config["ServerConf:NewApiUrl"] + : _config["ServerConf:OldApiUrl"]; + + if (string.IsNullOrEmpty(destBase)) + { + context.Response.StatusCode = 502; + await context.Response.WriteAsync("Destination not configured"); + return; + } + + if (!destBase.EndsWith("/")) destBase += "/"; + // salva path originale per ripristino + var originalPath = context.Request.Path; + var originalPathBase = context.Request.PathBase; + + try + { + string sKey = $"{target}|{metodo}|{id}"; + + // Registra scelta + _stats.Record(sKey); + + // imposta la Path che vogliamo che YARP appenda al destBase + context.Request.Path = new PathString("/" + relativePath); + // opzionale: se vuoi che PathBase sia vuoto per il backend, impostalo così + context.Request.PathBase = PathString.Empty; + + Log.Debug($"Forwarding to base {destBase} with forwarded path {context.Request.Path} | original PathBase:{originalPathBase} | Path={originalPath})"); + + var error = await _forwarder.SendAsync(context, destBase, _httpClientInvoker, _forwarderConfig, _transformer, context.RequestAborted); + + sw.Stop(); + _stats.RecordDuration(sKey, sw.Elapsed); + + if (error != ForwarderError.None) + { + var feat = context.GetForwarderErrorFeature(); + Log.Error(feat?.Exception, "Forwarder error to {DestBase}", destBase); + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 502; + await context.Response.WriteAsync($"Forward error: {feat?.Exception?.Message}"); + } + } + } + finally + { + // ripristina i path originali per non rompere la pipeline successiva + context.Request.Path = originalPath; + context.Request.PathBase = originalPathBase; + } + } + + private bool DecideByWeights(int oldW, int newW) + { + bool result = false; + // se entrambi zero -> prefer legacy + var total = oldW + newW; + if (total <= 0) + { + result = false; + } + else + { + var rnd = Random.Shared.NextDouble(); // 0..1 + result = rnd < (double)newW / total; + } + return result; + } + } +} diff --git a/MP-RIOC/Services/RouteStatsManager.cs b/MP-RIOC/Services/RouteStatsManager.cs new file mode 100644 index 00000000..8bb5caed --- /dev/null +++ b/MP-RIOC/Services/RouteStatsManager.cs @@ -0,0 +1,71 @@ +using System.Collections.Concurrent; + +namespace MP.RIOC.Services +{ + public class RouteStats + { + public long Count; + public TimeSpan TotalDuration = TimeSpan.Zero; + public TimeSpan MaxDuration = TimeSpan.Zero; + public TimeSpan MinDuration = TimeSpan.MaxValue; + public ConcurrentDictionary StatusCodes = new(); + } + + public class RouteStatsManager + { + private readonly ConcurrentDictionary _map = new(); + + /// + /// Registrazione del metodo + destinazione + /// + /// + public void Record(string dest_method) + { + var stat = _map.GetOrAdd(dest_method, _ => new RouteStats()); + Interlocked.Increment(ref stat.Count); + } + + /// + /// Registrazione destinazione+metodo + /// + /// chiave dest+metodo x salvataggio statistiche + /// + public void RecordDuration(string dest_method, TimeSpan duration) + { + if (_map.TryGetValue(dest_method, out var stat)) + { + lock (stat) + { + stat.TotalDuration += duration; + + if (stat.MaxDuration < duration) + { + stat.MaxDuration = duration; + } + if (stat.MinDuration > duration) + { + stat.MinDuration = duration; + } + } + } + } + + public void RecordStatusCode(string method, int statusCode) + { + if (_map.TryGetValue(method, out var stat)) + { + stat.StatusCodes.AddOrUpdate(statusCode, 1, (_, v) => v + 1); + } + } + + public IReadOnlyDictionary Snapshot() + { + return _map.ToDictionary(kv => kv.Key, kv => kv.Value); + } + + public void Clear() + { + _map.Clear(); + } + } +} diff --git a/MP-RIOC/WeatherForecast.cs b/MP-RIOC/WeatherForecast.cs new file mode 100644 index 00000000..86a54878 --- /dev/null +++ b/MP-RIOC/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace MP.RIOC +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/MP-RIOC/appsettings.Development.json b/MP-RIOC/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/MP-RIOC/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MP-RIOC/appsettings.json b/MP-RIOC/appsettings.json new file mode 100644 index 00000000..4583eef3 --- /dev/null +++ b/MP-RIOC/appsettings.json @@ -0,0 +1,79 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Yarp": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "NLog": { + "variables": { + "baseFileDir": "${basedir}/logs/", + "layout": "${longdate} | ${uppercase:${level}} | ${logger:shortName=false} | ${message}" + }, + "extensions": [ + { "assembly": "NLog.Extensions.Logging" }, + { "assembly": "NLog.Web.AspNetCore" } + ], + "throwConfigExceptions": true, + "targets": { + "async": true, + "logfile": { + "type": "File", + "fileName": "${basedir}/logs/${shortdate}.log", + "archiveEvery": "Day", + "archiveFileName": "${basedir}/logs/old/${shortdate}_{#}.log", + "archiveNumbering": "DateAndSequence", + "archiveAboveSize": "10240000", + "archiveDateFormat": "HH", + "maxArchiveFiles": "60", + "maxArchiveDays": "30" + }, + "logconsole": { + "type": "ColoredConsole", + "layout": "${longdate} | ${uppercase:${level}} | ${logger:shortName=true} | ${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Trace", + "writeTo": "logconsole" + }, + { + "logger": "*", + "minLevel": "Info", + "writeTo": "logfile" + } + ] + }, + "CodApp": "MP.RIOC", + "RouteMan": { + "MetricCalcIntervalSeconds": 10, + "MetricFlushIntervalSeconds": 60, + "DefaultWeightOld": 100, + "DefaultWeightNew": 0, + "DeleteExpiredMetrics": true + }, + "ServerConf": { + "RoutePath": "/api/IOB", + "HttpVersion": "1.1", + "HttpVersionPolicy": "RequestVersionExact", + "OldApiUrl": "https://iis01.egalware.com/MP/IO/IOB/", + "NewApiUrl": "https://iis01.egalware.com/MP/IOC/api/IOB/", + "BaseUrlIoc": "/MP/RIOC/", + "MpIoNS": "MoonPro:SQL2016DEV:MoonPro", + "RedisBaseKey": "MP-IOC", + "RedisWeight": true, + "SafePages": "Index", + "redisLongTimeCache": 60, + "redisShortTimeCache": 30, + "useFactory": true + }, + "ConnectionStrings": { + "MP.Utils": "Server=SQL2016DEV;Database=MoonPro_Utils; User ID=sa;Password=keyhammer16; integrated security=False; App=MP.IOC;", + "Redis": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false", + "RedisAdmin": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false,allowAdmin=true" + } +} diff --git a/MP-RIOC/compilerconfig.json b/MP-RIOC/compilerconfig.json new file mode 100644 index 00000000..445819db --- /dev/null +++ b/MP-RIOC/compilerconfig.json @@ -0,0 +1,6 @@ +[ + { + "outputFile": "Components/Layout/MainLayout.razor.css", + "inputFile": "Components/Layout/MainLayout.razor.less" + } +] \ No newline at end of file diff --git a/MP-RIOC/compilerconfig.json.defaults b/MP-RIOC/compilerconfig.json.defaults new file mode 100644 index 00000000..a5a10c08 --- /dev/null +++ b/MP-RIOC/compilerconfig.json.defaults @@ -0,0 +1,59 @@ +{ + "compilers": { + "less": { + "autoPrefix": "", + "cssComb": "none", + "ieCompat": true, + "math": null, + "strictMath": false, + "strictUnits": false, + "relativeUrls": true, + "rootPath": "", + "sourceMapRoot": "", + "sourceMapBasePath": "", + "sourceMap": false + }, + "sass": { + "autoPrefix": "", + "loadPaths": "", + "style": "expanded", + "relativeUrls": true, + "sourceMap": false + }, + "stylus": { + "sourceMap": false + }, + "babel": { + "sourceMap": false + }, + "coffeescript": { + "bare": false, + "runtimeMode": "node", + "sourceMap": false + }, + "handlebars": { + "root": "", + "noBOM": false, + "name": "", + "namespace": "", + "knownHelpersOnly": false, + "forcePartial": false, + "knownHelpers": [], + "commonjs": "", + "amd": false, + "sourceMap": false + } + }, + "minifiers": { + "css": { + "enabled": true, + "termSemicolons": true, + "gzip": false + }, + "javascript": { + "enabled": true, + "termSemicolons": true, + "gzip": false + } + } +} \ No newline at end of file diff --git a/MP-RIOC/post-build.ps1 b/MP-RIOC/post-build.ps1 new file mode 100644 index 00000000..446eede8 --- /dev/null +++ b/MP-RIOC/post-build.ps1 @@ -0,0 +1,32 @@ +param([string]$ProjectDir, [string]$ProjectPath); + +$FileMajMin = "..\MajMin.vers" +$FileVers = "Resources\VersNum.txt" +$FileManIn = "Resources\manifest-original.xml" +$FileManOut = "Resources\manifest.xml" +$FileCLogIn = "Resources\ChangeLog-original.html" +$FileCLogOut = "Resources\ChangeLog.html" +$MajMin = Get-Content $FileMajMin +$currentDate = get-date -format yyMM; +$currentTime = get-date -format dHH; +$find = "(.|\n)*?"; +$currRelNum = $MajMin + $currentDate +"." + $currentTime +$replace = "" + $MajMin + $currentDate +"." + $currentTime + ""; +$csproj = Get-Content $ProjectPath +$csprojUpdated = $csproj -replace $find, $replace + +Set-Content -Path $ProjectPath -Value $csprojUpdated +Set-Content -Path $FileVers -Value $currRelNum + +# replace x manifest +$manData = Get-Content $FileManIn +$manData = $manData -replace "1.0.0.0", $currRelNum +$manData = $manData -replace "{{DIRNAME}}", "MP-IOC" +$manData = $manData -replace "{{BRANCHNAME}}", "stable/LAST" +$manData = $manData -replace "{{PACKNAME}}", "MP.IOC" +Set-Content -Path $FileManOut -Value $manData + +# replace x ChangeLog +$clogData = Get-Content $FileCLogIn +$clogData = $clogData -replace "{{CURRENT-REL}}", $currRelNum +Set-Content -Path $FileCLogOut -Value $clogData diff --git a/MP.IOC/Program.cs b/MP.IOC/Program.cs index c0ccbfda..62c3e23e 100644 --- a/MP.IOC/Program.cs +++ b/MP.IOC/Program.cs @@ -35,7 +35,18 @@ logger.Info("Setup REDIS OK"); // YARP base config -builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); +builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) + .ConfigureHttpClient((context, handler) => + { + // Se sei in sviluppo o sul server con problemi di certificato + handler.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => + { + // ATTENZIONE: In produzione filtra per hostname o usa cautela + return true; + }; + }); + builder.Services.AddHttpForwarder(); // HttpMessageInvoker (SocketsHttpHandler) diff --git a/MP.IOC/appsettings.json b/MP.IOC/appsettings.json index d7c5727c..0600459a 100644 --- a/MP.IOC/appsettings.json +++ b/MP.IOC/appsettings.json @@ -2,6 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", + "Yarp": "Debug", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Warning", "Microsoft.EntityFrameworkCore.Infrastructure": "Warning", @@ -64,9 +65,17 @@ ], "Clusters": { "cluster-old": { + "HttpRequest": { + "Version": "1.1", + "VersionPolicy": "RequestVersionExact" + }, "Destinations": { "old1": { "Address": "https://iis01.egalware.com/MP/IO/IOB/" } } }, "cluster-new": { + "HttpRequest": { + "Version": "1.1", + "VersionPolicy": "RequestVersionExact" + }, "Destinations": { "new1": { "Address": "https://iis01.egalware.com/MP/IOC/api/IOB/" } } } }