Update pesante x gestione servizio x accesso IOC repo

This commit is contained in:
Samuele Locatelli
2026-04-30 17:58:55 +02:00
parent 7fe91d59d7
commit 523bf78d33
9 changed files with 1184 additions and 19 deletions
+75
View File
@@ -1,5 +1,6 @@
using MP.Data.DbModels;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static MP.Core.Objects.Enums;
@@ -26,6 +27,18 @@ namespace MP.Data.Repository.IOC
/// <returns></returns>
Task<bool> CheckCambiaStatoBatchAsync(tipoInputEvento tipoInput, string IdxMacchina, DateTime InizioStato, int IdxTipo, string CodArt, string Value, int MatrOpr, string pallet);
/// <summary>
/// Elenco da tabella Config
/// </summary>
/// <returns></returns>
Task<List<ConfigModel>> ConfigGetAllAsync();
/// <summary>
/// Intera tab dati macchina
/// </summary>
/// <returns></returns>
Task<List<DatiMacchineModel>> DatiMacchineGetAllAsync();
/// <summary>
/// Aggiunta record MicroStato + EventList
/// </summary>
@@ -34,6 +47,41 @@ namespace MP.Data.Repository.IOC
/// <returns></returns>
Task<bool> EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv);
/// <summary>
/// Upsert record keepalive
/// </summary>
/// <param name="IdxMacc"></param>
/// <param name="OraServer"></param>
/// <param name="OraMacc"></param>
/// <returns></returns>
Task<bool> KeepAliveUpsertAsync(string IdxMacc, DateTime OraServer, DateTime OraMacc);
/// <summary>
/// Intera tabella relazione master/slave in machine (gestione setup master - slave)
/// </summary>
/// <returns></returns>
Task<List<Macchine2SlaveModel>> Macchine2SlaveAsync();
/// <summary>
/// Recupera record macchina da Idx
/// </summary>
/// <param name="IdxMacchina"></param>
/// <returns></returns>
Task<MacchineModel?> MacchineGetByIdxAsync(string IdxMacchina);
/// <summary>
/// Upsert Record Macchine ASYNC
/// </summary>
/// <returns></returns>
Task<bool> MacchineUpsertAsync(MacchineModel entity);
/// <summary>
/// Elenco da tabella Macchine
/// </summary>
/// <param name="IdxMacc"></param>
/// <returns></returns>
Task<List<MicroStatoMacchinaModel>> MicroStatoMacchinaGetByIdxMaccAsync(string IdxMacc);
/// <summary>
/// Aggiornamento record Microstato macchina
/// </summary>
@@ -41,6 +89,33 @@ namespace MP.Data.Repository.IOC
/// <returns></returns>
Task<bool> MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec);
/// <summary>
/// Aggiunta record SignalLog Async
/// </summary>
/// <param name="newRec"></param>
/// <returns></returns>
Task<bool> SignalLogInsertAsync(SignalLogModel newRec);
/// <summary>
/// Intera tabella state machine ingressi 2 eventi
/// </summary>
/// <returns></returns>
Task<List<TransizioneIngressiModel>> StateMachineIngressiAsync(int idxFam);
/// <summary>
/// Vista v_MSFD x singola macchina (da stored) - singolo record
/// </summary>
/// <param name="idxMacc"></param>
/// <returns></returns>
Task<VMSFDModel?> VMSFDGetByMaccAsync(string idxMacc);
/// <summary>
/// Vista v_MSFD delle machine MULTI filtrato x macchina (da stored)
/// </summary>
/// <param name="idxMacc"></param>
/// <returns></returns>
Task<List<VMSFDModel>> VMSFDGetMultiByMaccAsync(string idxMacc);
#endregion Public Methods
}
}
+170 -1
View File
@@ -1,8 +1,10 @@
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
using MP.Data.DbModels;
using NLog;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
@@ -99,6 +101,30 @@ namespace MP.Data.Repository.IOC
}
}
/// <inheritdoc />
public async Task<List<ConfigModel>> ConfigGetAllAsync()
{
await using var dbCtx = await CreateContextAsync();
var dbResult = await dbCtx
.DbSetConfig
.AsNoTracking()
.OrderBy(x => x.Chiave)
.ToListAsync();
return dbResult;
}
/// <inheritdoc />
public async Task<List<DatiMacchineModel>> DatiMacchineGetAllAsync()
{
await using var dbCtx = await CreateContextAsync();
var dbResult = await dbCtx
.DbSetDatiMacchine
.AsNoTracking()
.OrderBy(x => x.IdxMacchina)
.ToListAsync();
return dbResult;
}
/// <inheritdoc />
public async Task<bool> EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv)
{
@@ -145,10 +171,103 @@ namespace MP.Data.Repository.IOC
}
}
/// <inheritdoc />
public async Task<bool> KeepAliveUpsertAsync(string IdxMacc, DateTime OraServer, DateTime OraMacc)
{
await using var dbCtx = await CreateContextAsync();
bool fatto = false;
var currRec = await dbCtx
.DbSetKeepAlive
.Where(x => x.IdxMacchina == IdxMacc)
.FirstOrDefaultAsync();
if (currRec != null)
{
currRec.DataOraServer = OraServer;
currRec.DataOraMacchina = OraMacc;
dbCtx.Entry(currRec).State = EntityState.Modified;
}
else
{
KeepAliveModel newRec = new KeepAliveModel()
{
IdxMacchina = IdxMacc,
DataOraMacchina = OraMacc,
DataOraServer = OraServer,
DataOraStart = DateTime.Now
};
dbCtx
.DbSetKeepAlive
.Add(newRec);
}
fatto = await dbCtx.SaveChangesAsync() > 0;
return fatto;
}
/// <inheritdoc />
public async Task<List<Macchine2SlaveModel>> Macchine2SlaveAsync()
{
await using var dbCtx = await CreateContextAsync();
List<Macchine2SlaveModel> dbResult = await dbCtx
.DbSetM2S
.AsNoTracking()
.OrderBy(x => x.IdxMacchina)
.ToListAsync();
return dbResult;
}
/// <inheritdoc />
public async Task<MacchineModel?> MacchineGetByIdxAsync(string IdxMacchina)
{
await using var dbCtx = await CreateContextAsync();
MacchineModel dbResult = await dbCtx
.DbSetMacchine
.FirstOrDefaultAsync(x => x.IdxMacchina == IdxMacchina);
return dbResult;
}
/// <inheritdoc />
public async Task<bool> MacchineUpsertAsync(MacchineModel entity)
{
await using var dbCtx = await CreateContextAsync();
// Recuperiamo l'entità tracciata dal context
var trackedEntity = await dbCtx
.DbSetMacchine
.FirstOrDefaultAsync(x => x.IdxMacchina == entity.IdxMacchina);
if (trackedEntity != null)
{
// Aggiorna i valori dell'entità tracciata con quelli della nuova
dbCtx.Entry(trackedEntity).CurrentValues.SetValues(entity);
}
else
{
dbCtx.DbSetMacchine.Update(entity);
}
return await dbCtx.SaveChangesAsync() > 0;
}
/// <inheritdoc />
public async Task<List<MicroStatoMacchinaModel>> MicroStatoMacchinaGetByIdxMaccAsync(string IdxMacc)
{
await using var dbCtx = await CreateContextAsync();
List<MicroStatoMacchinaModel> dbResult = new List<MicroStatoMacchinaModel>();
dbResult = await dbCtx
.DbSetMicroStatoMacc
.Where(x => x.IdxMacchina == IdxMacc)
.AsNoTracking()
.ToListAsync();
return dbResult;
}
/// <inheritdoc />
public async Task<bool> MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec)
{
bool fatto = false;
await using var dbCtx = await CreateContextAsync();
bool fatto = false;
var actRec = await dbCtx
.DbSetMicroStatoMacc
@@ -174,6 +293,56 @@ namespace MP.Data.Repository.IOC
return fatto;
}
/// <inheritdoc />
public async Task<bool> SignalLogInsertAsync(SignalLogModel newRec)
{
await using var dbCtx = await CreateContextAsync();
var currRec = dbCtx
.DbSetSignalLog
.Add(newRec);
return await dbCtx.SaveChangesAsync() > 0;
}
/// <inheritdoc />
public async Task<List<TransizioneIngressiModel>> StateMachineIngressiAsync(int idxFam)
{
await using var dbCtx = await CreateContextAsync();
var IdxFamIn = new SqlParameter("@IdxFamigliaIngresso", idxFam);
var dbResult = await dbCtx
.DbSetSMI
.FromSqlRaw("exec dbo.stp_TRI_getByIdxFamIng @IdxFamigliaIngresso", IdxFamIn)
.AsNoTracking()
.ToListAsync();
return dbResult;
}
/// <inheritdoc />
public async Task<VMSFDModel?> VMSFDGetByMaccAsync(string idxMacc)
{
await using var dbCtx = await CreateContextAsync();
var IdxMacchina = new SqlParameter("@IdxMacchina", idxMacc);
var dbResult = (await dbCtx
.DbSetMSFD
.FromSqlRaw("exec dbo.stp_MSFD_getMacc @IdxMacchina", IdxMacchina)
.AsNoTracking()
.ToListAsync())
.FirstOrDefault();
return dbResult;
}
/// <inheritdoc />
public async Task<List<VMSFDModel>> VMSFDGetMultiByMaccAsync(string idxMacc)
{
await using var dbCtx = await CreateContextAsync();
var IdxMacchina = new SqlParameter("@IdxMacchina", idxMacc);
var dbResult = await dbCtx
.DbSetMSFD
.FromSqlRaw("exec dbo.stp_MSFD_getMulti @IdxMacchina", IdxMacchina)
.AsNoTracking()
.ToListAsync();
return dbResult;
}
#endregion Public Methods
#region Protected Fields
+11
View File
@@ -38,6 +38,17 @@ namespace MP.Data.Services.IOC
/// <returns></returns>
Task<bool> MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec);
/// <summary>
/// Processa input da IOB eventualmente registrando i segnali inviati
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="valore"></param>
/// <param name="dtEve"></param>
/// <param name="dtCurr"></param>
/// <param name="contatore"></param>
/// <returns></returns>
Task<string> ProcessInputAsync(string idxMacchina, string valore, string dtEve, string dtCurr, string contatore);
#endregion Public Methods
}
}
+909 -1
View File
@@ -1,8 +1,15 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MP.Core.Objects;
using MP.Data.DbModels;
using MP.Data.Repository.IOC;
using Newtonsoft.Json;
using NLog;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using static MP.Core.Objects.Enums;
@@ -15,10 +22,14 @@ namespace MP.Data.Services.IOC
public IocService(
IConfiguration config,
IConnectionMultiplexer redis,
IIocRepository repo) : base(config, redis)
IIocRepository repo,
IServiceScopeFactory scopeFactory) : base(config, redis)
{
_className = "IocServ";
int.TryParse(config.GetValue<string>("ServerConf:redisLongTimeCache"), out redisLongTimeCache);
int.TryParse(config.GetValue<string>("ServerConf:redisShortTimeCache"), out redisShortTimeCache);
_repo = repo;
_scopeFactory = scopeFactory;
}
#endregion Public Constructors
@@ -32,6 +43,37 @@ namespace MP.Data.Services.IOC
return success;
}
/// <summary>
/// Elenco completo config da DB
/// </summary>
/// <returns></returns>
public async Task<List<ConfigModel>> ConfigGetAllAsync()
{
List<ConfigModel>? result = new List<ConfigModel>();
// cerco in redis...
RedisValue rawData = await _redisDb.StringGetAsync(MP.Data.Utils.redisConfKey);
string source = "DB";
if (!string.IsNullOrEmpty($"{rawData}"))
{
result = JsonConvert.DeserializeObject<List<ConfigModel>>($"{rawData}");
source = "REDIS";
}
else
{
result = await _repo.ConfigGetAllAsync();
//result = await Task.FromResult(SpecDbController.ConfigGetAllAsync());
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
await _redisDb.StringSetAsync(MP.Data.Utils.redisConfKey, rawData, getRandTOut(redisLongTimeCache));
}
Log.Debug($"ConfigGetAllAsync Read from {source}");
if (result == null)
{
result = new List<ConfigModel>();
}
return result;
}
/// <inheritdoc />
public async Task<bool> EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv)
{
@@ -39,6 +81,66 @@ namespace MP.Data.Services.IOC
return success;
}
/// <summary>
/// Restituisce il valOut booleano se la macchina sia abilitata all'inserimento COMPLETO nel
/// Signal Log
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public async Task<bool> IobSLogEnabAsync(string idxMacchina)
{
bool answ = false;
// ORA recupero da memoria redis...
try
{
var currHash = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS);
RedisValue rawData = await _redisDb.HashGetAsync(currHash, (RedisValue)"sLogEnabled");
// se è vuoto... leggo da DB e popolo!
if (!rawData.HasValue)
{
await ResetDatiMacchinaAsync(idxMacchina);
// riprovo
rawData = await _redisDb.HashGetAsync(currHash, (RedisValue)"sLogEnabled");
}
// provo conversione
bool.TryParse($"{rawData}", out answ);
}
catch (Exception exc)
{
Log.Error($"Errore IobSLogEnabAsync | idxMacchina {idxMacchina}:{Environment.NewLine}{exc}");
}
return answ;
}
/// <summary>
/// Elenco completo valori Macchine 2 Slave
/// </summary>
/// <returns></returns>
public async Task<List<Macchine2SlaveModel>> Macchine2SlaveGetAllAsync()
{
List<Macchine2SlaveModel>? result = new List<Macchine2SlaveModel>();
string currKey = $"{MP.Data.Utils.redisBaseAddr}:M2STab";
// cerco in redis dato valOut sel macchina...
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<Macchine2SlaveModel>>($"{rawData}");
}
else
{
result = await _repo.Macchine2SlaveAsync();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10));
}
if (result == null)
{
result = new List<Macchine2SlaveModel>();
}
return result;
}
/// <inheritdoc />
public async Task<bool> MicroStatoMacchinaUpsertAsync(MicroStatoMacchinaModel newRec)
{
@@ -46,13 +148,819 @@ namespace MP.Data.Services.IOC
return success;
}
/// <inheritdoc />
public async Task<string> 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<string, string> 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;
}
/// <summary>
/// scrive un evento di keepalive sulla tabella
/// </summary>
/// <param name="IdxMacchina"></param>
/// <param name="oraMacchina"></param>
/// <returns></returns>
public async Task ScriviKeepAliveAsync(string IdxMacchina, DateTime oraMacchina)
{
// cerco se ho keep alive in redis,
var currKey = MP.Data.Utils.RedKeyHash($"KeepAlive:{IdxMacchina}");
bool keyPresent = await _redisDb.KeyExistsAsync(currKey);
// se NON presente salvo in REDIS con TTL 10 sec e sul DB...
if (!keyPresent)
{
DateTime adesso = DateTime.Now;
await _redisDb.StringSetAsync(currKey, adesso.ToString("s"), TimeSpan.FromSeconds(30));
// effettuo scrittura sul DB
await _repo.KeepAliveUpsertAsync(IdxMacchina, DateTime.Now, oraMacchina);
}
}
#endregion Public Methods
#region Protected Fields
protected Random rand = new Random();
#endregion Protected Fields
#region Protected Methods
/// <summary>
/// Restituisce un timeout dai minuti richiesti + tempo random 1..60 sec
/// </summary>
/// <param name="stdMinutes"></param>
/// <returns></returns>
protected TimeSpan getRandTOut(double stdMinutes)
{
double rndValue = stdMinutes + (double)rand.Next(1, 60) / 60;
return TimeSpan.FromMinutes(rndValue);
}
#endregion Protected Methods
#region Private Fields
private static Logger Log = LogManager.GetCurrentClassLogger();
private readonly string _className;
private readonly IIocRepository _repo;
private readonly IServiceScopeFactory _scopeFactory;
/// <summary>
/// Provider CultureInfo x parse valori (es dataora)
/// </summary>
private CultureInfo ciProvider = CultureInfo.InvariantCulture;
/// <summary>
/// Formato dataora standard x parsing
/// </summary>
private string dtFormat = "yyyyMMddHHmmssfff";
private int redisLongTimeCache = 5;
private int redisShortTimeCache = 2;
#endregion Private Fields
#region Private Methods
/// <summary>
/// controlla se da il segnale di "microstato" deriva un evento da generare - modalità OFFLINE
/// </summary>
/// <param name="idxMacchina">idx macchina</param>
/// <param name="valore">valOut ingresso</param>
/// <param name="dtEve">data-ora evento (server)</param>
/// <param name="contatore">sequenza dati inviati</param>
/// <param name="datiMaccCache">dati macchina in cache (opzionale, se null fa lookup)</param>
/// <returns></returns>
private async Task<inputComandoMapo> CheckMicroStatoAsync(string idxMacchina, string valore, DateTime dtEve, string contatore, Dictionary<string, string>? datiMaccCache = null)
{
// recupero SE IMPIEGATO REDIS i valori del Dictionary della macchina...
Dictionary<string, string> datiMacc = datiMaccCache ?? await mDatiMacchineAsync(idxMacchina);
// processing
inputComandoMapo answ = new inputComandoMapo();
// SE no trovassi verifico se esista la macchina altrimenti la creo... REDIS compliant
if (!datiMacc.ContainsKey(idxMacchina))
{
await verificaIdxMacchinaAsync(idxMacchina);
}
// continuo processing...
string CodArticolo = datiMacc["CodArticolo"];
if (string.IsNullOrEmpty(CodArticolo))
{
var allDatiMacch = await _repo.DatiMacchineGetAllAsync();
var recMacc = allDatiMacch.FirstOrDefault(x => x.IdxMacchina == idxMacchina);
if (recMacc != null)
{
CodArticolo = recMacc.CodArticoloA;
}
}
// preparo gestione val ingresso
int? valINT = 0;
int idxTipoEv = 0;
int next_idxMS = 0;
try
{
valINT = int.Parse(valore, NumberStyles.HexNumber);
int idxFamIn = Convert.ToInt32(datiMacc["IdxFamIn"]);
int idxMicroStato = Convert.ToInt32(datiMacc["IdxMicroStato"]);
int valIOB = Convert.ToInt32(valINT);
next_idxMS = idxMicroStato;
// recupero singolo valOut (stringa) x chiave
string todoSMI = await ValoreSmiAsync(idxFamIn, idxMicroStato, valIOB);
// solo se ho trovato un risultato nella tab SMI della famiglia macchina...
if (!string.IsNullOrEmpty(todoSMI) && todoSMI.Contains("_"))
{
// splitto e salvo valori OUT...
string[] valori = todoSMI.Split('_');
idxTipoEv = Convert.ToInt32(valori[0]);
next_idxMS = Convert.ToInt32(valori[1]);
}
}
catch (Exception exc)
{
Log.Error($"[ChkMiSt_5a] - - Eccezione in recupero riga Trans ingressi | idxMacchina {idxMacchina} | valINT {valINT}:{Environment.NewLine}{exc}", Environment.NewLine, exc, valINT, idxMacchina);
}
// effettuo update vari SU DB!!!
MicroStatoMacchinaModel newRecMsm = new MicroStatoMacchinaModel()
{
IdxMacchina = idxMacchina,
IdxMicroStato = next_idxMS,
InizioStato = dtEve,
Value = valore
};
if (idxTipoEv > 0)
{
// preparo record
string valEsteso = string.Format("[{0}] {1}", contatore.PadLeft(3, '0'), valore);
// creo evento
EventListModel newRecEv = new EventListModel()
{
CodArticolo = CodArticolo,
IdxMacchina = idxMacchina,
IdxTipo = idxTipoEv,
InizioStato = dtEve,
MatrOpr = 0,
pallet = "-",
Value = valEsteso
};
// salva e processa evento + microstato
answ = await scriviRigaEventoAsync(newRecMsm, newRecEv);
}
else
{
await _repo.MicroStatoMacchinaUpsertAsync(newRecMsm);
}
return answ;
}
/// <summary>
/// Helper standardizzazione valOut dataora ricevuto da IOB remoti
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
private string dtFormStd(string? s)
{
return s?.Length > 17 ? s[..17] : s?.PadRight(17, '0') ?? string.Empty;
}
/// <summary>
/// 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
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="datiMacc"></param>
/// <returns></returns>
private bool isMulti(string idxMacchina, Dictionary<string, string> 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<HashSet<string>> ListMasterAsync()
{
HashSet<string> result = new();
string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListMaster";
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<HashSet<string>>($"{rawData}") ?? new();
}
else
{
var fullList = await Macchine2SlaveGetAllAsync();
result = fullList.Select(x => x.IdxMacchina).ToHashSet<string>();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10));
}
return result;
}
private async Task<HashSet<string>> ListSlaveAsync()
{
HashSet<string> result = new();
string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListSlave";
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<HashSet<string>>($"{rawData}") ?? new();
}
else
{
var fullList = await Macchine2SlaveGetAllAsync();
result = fullList.Select(x => x.IdxMacchinaSlave).Distinct().ToHashSet<string>();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10));
}
return result;
}
/// <summary>
/// Restitusice elenco KVP dei campi DatiMacchine + StatoMacchine per l'impianto indicato
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
private async Task<Dictionary<string, string>> mDatiMacchineAsync(string idxMacchina)
{
// hard coded dimensione vettore DatiMacchine
Dictionary<string, string> answ = new Dictionary<string, string>();
// ORA recupero da memoria redis...
try
{
var currHash = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS);
answ = await RedisGetHashDictAsync(currHash);
// se è vuoto... leggo da DB e popolo!
if (answ.Count == 0)
{
answ = await ResetDatiMacchinaAsync(idxMacchina);
}
}
catch (Exception exc)
{
Log.Error($"Errore in compilazione dati Macchine x Redis - idxMacchina {idxMacchina}:{Environment.NewLine}{exc}");
}
return answ;
}
/// <summary>
/// Restitusice elenco KVP (async) per evitare blocchi quando chiamato da metodi asincroni
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
private async Task<KeyValuePair<string, string>[]> mTabMSMIAsync(string idxMacchina)
{
KeyValuePair<string, string>[] answ = new KeyValuePair<string, string>[1];
answ[0] = new KeyValuePair<string, string>("0", "0");
try
{
var currHash = MP.Data.Utils.RedKeyMsmi(idxMacchina);
answ = await RedisGetHashAsync(currHash);
if (answ == null || answ.Length == 0)
{
answ = await resetMSMIAsync(idxMacchina);
}
}
catch (Exception exc)
{
Log.Error($"Errore in compilazione Tabella Multi State Machine Ingressi x Redis:{Environment.NewLine}{exc}");
}
return answ;
}
/// <summary>
/// Calcola dataora evento da info dt ricevute
/// </summary>
/// <param name="dtEve"></param>
/// <param name="dtCurr"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Calcola l'effettivo valOut da passare alla macchina a stati INGRESSI usando dati macchina in cache
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="valore"></param>
/// <param name="datiMacc"></param>
/// <returns></returns>
private string preProcInput(string idxMacchina, string valore, Dictionary<string, string> 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<KeyValuePair<string, string>[]> RedisGetHashAsync(RedisKey redKey)
{
HashEntry[] rawData = await _redisDb.HashGetAllAsync(redKey);
var result = rawData.Where(x => !x.Name.IsNull).Select(x => new KeyValuePair<string, string>($"{x.Name}", $"{x.Value}")).ToArray();
return result;
}
private async Task<Dictionary<string, string>> RedisGetHashDictAsync(RedisKey hashKey)
{
HashEntry[] rawData = await _redisDb.HashGetAllAsync(hashKey);
var result = rawData
.Where(x => !x.Name.IsNull)
.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString());
return result;
}
private async Task RedisSetHashAsync(RedisKey redKey, KeyValuePair<string, string>[] 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));
}
}
/// <summary>
/// Restitusice elenco KVP dei campi DatiMacchine + StatoMacchine per l'impianto indicato
/// </summary>
/// <param name="idxMacc"></param>
/// <returns></returns>
private async Task<Dictionary<string, string>> ResetDatiMacchinaAsync(string idxMacc)
{
Dictionary<string, string> result = new Dictionary<string, string>();
VMSFDModel? dbResult = null;
dbResult = await _repo.VMSFDGetByMaccAsync(idxMacc);
if (dbResult == null) return new Dictionary<string, string>();
double numSecCache = redisLongTimeCache;
// converto in formato dizionario...
if (dbResult != null)
{
// salvo 1:1 i valori... STATO
result.Add("IdxMicroStato", $"{dbResult.IdxMicroStato}");
result.Add("IdxStato", $"{dbResult.IdxStato}");
result.Add("CodArticolo", $"{dbResult.CodArticolo}");
result.Add("insEnabled", $"{dbResult.InsEnabled}");
result.Add("sLogEnabled", $"{dbResult.SLogEnabled}");
result.Add("pallet", $"{dbResult.Pallet}");
result.Add("CodArticolo_A", $"{dbResult.CodArticoloA}");
result.Add("CodArticolo_B", $"{dbResult.CodArticoloB}");
result.Add("TempoCicloBase", $"{dbResult.TempoCicloBase}");
result.Add("PzPalletProd", $"{dbResult.PzPalletProd}");
result.Add("MatrOpr", $"{dbResult.MatrOpr}");
result.Add("lastVal", $"{dbResult.LastVal}");
result.Add("TCBase", $"{dbResult.TempoCicloBase}");
//...e SETUP
result.Add("CodMacc", $"{dbResult.Codmacchina}");
result.Add("IdxFamIn", $"{dbResult.IdxFamigliaIngresso}");
result.Add("Multi", $"{dbResult.Multi}");
result.Add("BitFilt", $"{dbResult.BitFilt}");
result.Add("MaxVal", $"{dbResult.MaxVal}");
result.Add("BSR", $"{dbResult.Bsr}");
result.Add("ExplodeBit", $"{dbResult.ExplodeBit}");
result.Add("NumBit", $"{dbResult.NumBit}");
result.Add("IdxFamMacc", $"{dbResult.IdxFamiglia}");
result.Add("simplePallet", $"{dbResult.SimplePallet}");
result.Add("palletChange", $"{dbResult.PalletChange}");
// durata cache in secondi dal valOut insEnabled...
//double numSecCache = ((result["insEnabled"].ToLower() == "true") ? redisShortTimeCache : redisLongTimeCache);
numSecCache = dbResult.InsEnabled ? redisShortTimeCache : redisLongTimeCache;
}
else
{
}
// dati master/slave
string isMaster = (await ListMasterAsync()).Contains(idxMacc) ? "1" : "0";
string isSlave = (await ListSlaveAsync()).Contains(idxMacc) ? "1" : "0";
result.Add("Master", isMaster);
result.Add("Slave", isSlave);
// Processing redis in transazoine...
var redKey = MP.Data.Utils.RedKeyDatiMacc(idxMacc, MpIoNS);
// variazione con redis transaction...
var transaction = _redisDb.CreateTransaction();
// 1. Eliminiamo la chiave (rimuove i vecchi campi)
_ = transaction.KeyDeleteAsync(redKey);
// 2. Prepariamo gli HashEntry per il batch (più efficiente di un Dictionary)
HashEntry[] entries = result.Select(kvp => new HashEntry(kvp.Key, kvp.Value)).ToArray();
// 3. Inseriamo i nuovi valori
_ = transaction.HashSetAsync(redKey, entries);
// 4. Impostiamo l'expiration in un unico colpo
_ = transaction.KeyExpireAsync(redKey, TimeSpan.FromSeconds(numSecCache));
// Eseguiamo tutto in un unico viaggio verso Redis
bool success = await transaction.ExecuteAsync();
return result;
}
/// <summary>
/// Resetta (rileggendo) i dati della State Machine multi ingressi nel formato
/// currKey: IdxMacchina
/// value: IdxFamigliaIngresso
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
private async Task<KeyValuePair<string, string>[]> resetMSMIAsync(string idxMacchina)
{
var currHash = MP.Data.Utils.RedKeyMsmi(idxMacchina);
// recupero records
var tabMSMI = await _repo.VMSFDGetMultiByMaccAsync(idxMacchina);
KeyValuePair<string, string>[] answ = new KeyValuePair<string, string>[tabMSMI.Count];
// salvo tutti i valori StateMachineIngressi...
int i = 0;
foreach (var item in tabMSMI)
{
answ[i] = new KeyValuePair<string, string>(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;
}
/// <summary>
/// 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)
/// </summary>
/// <param name="idxFamIn"></param>
/// <returns></returns>
private async Task<KeyValuePair<string, string>[]> resetSMIAsync(int idxFamIn)
{
var currHash = MP.Data.Utils.GetHashSMI(idxFamIn);
// leggo da DB...
var tabSMI = await _repo.StateMachineIngressiAsync(idxFamIn);
KeyValuePair<string, string>[] answ = new KeyValuePair<string, string>[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<string, string>(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;
}
/// <summary>
/// salva il segnale di "microstato" (segnale) ASYNC
/// </summary>
/// <param name="idxMacchina">idx macchina</param>
/// <param name="valore">valOut ingresso</param>
/// <param name="dtEve">data-ora evento (server)</param>
/// <param name="contatore">contatore sequenza dati inviati</param>
/// <returns></returns>
private async Task<bool> saveSigLogAsync(string idxMacchina, string valore, DateTime dtEve, int contatore)
{
SignalLogModel newRec = new SignalLogModel()
{
IdxMacchina = idxMacchina,
DtCurr = DateTime.Now,
DtEve = dtEve,
Contatore = contatore,
Valore = valore
};
return await _repo.SignalLogInsertAsync(newRec);
}
/// <summary>
/// Scrive una riga evento + una riga microstato insieme, ed effettua verifica necessità cambio stato
/// </summary>
/// <param name="newRecMsm">Record MicroStatoMacchina</param>
/// <param name="newRecEv">record EventList</param>
/// <returns></returns>
private async Task<inputComandoMapo> scriviRigaEventoAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv)
{
bool inserito = false;
// inserisco evento
inserito = await _repo.EvListMicroStatoInsertAsync(newRecMsm, newRecEv);
// faccio controllo per eventuale cambio stato da tab transizioni...
await _repo.CheckCambiaStatoBatchAsync(tipoInputEvento.hw, newRecEv.IdxMacchina, newRecEv.InizioStato ?? DateTime.Now, newRecEv.IdxTipo, newRecEv.CodArticolo, newRecEv.Value, newRecEv.MatrOpr, newRecEv.pallet);
// formatto output
inputComandoMapo answ = new inputComandoMapo();
answ.outValue = inserito.ToString();
answ.needStatusRefresh = true;
return answ;
}
/// <summary>
/// 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
/// </summary>
/// <param name="idxFamIn"></param>
/// <returns></returns>
private async Task<KeyValuePair<string, string>[]> StateMachInByKeyAsync(int idxFamIn)
{
// hard coded dimensione vettore DatiMacchine
KeyValuePair<string, string>[] answ = new KeyValuePair<string, string>[1];
// iniziualizzo con un valOut... 0/0
answ[0] = new KeyValuePair<string, string>("0", "0");
// ORA recupero da memoria redis...
try
{
var currHash = MP.Data.Utils.GetHashSMI(idxFamIn);
answ = await RedisGetHashAsync(currHash);
// se è vuoto... leggo da DB e popolo!
if (answ.Length == 0)
{
answ = await resetSMIAsync(idxFamIn);
}
}
catch (Exception exc)
{
Log.Error($"Errore in compilazione State Machine Ingressi x Redis:{Environment.NewLine}{exc}");
}
return answ;
}
/// <summary>
/// Restituisce valOut della stringa (SE disponibile)
/// </summary>
/// <param name="keyName"></param>
/// <returns></returns>
private async Task<string> 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;
}
/// <summary>
/// Validazione preliminare valori input
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="valore"></param>
/// <param name="dtEve"></param>
/// <param name="dtCurr"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Restituisce il valore SPECIFICATO per la state machine ingressi
/// value: iTipoEv_nState (IdxTipoEv da trasmettere + New MICRO-STATE)
/// </summary>
/// <param name="idxFamIn"></param>
/// <param name="idxMicroStato"></param>
/// <param name="valoreIn"></param>
/// <returns></returns>
private async Task<string> ValoreSmiAsync(int idxFamIn, int idxMicroStato, int valoreIn)
{
string valOut = "";
var currHash = MP.Data.Utils.GetHashSMI(idxFamIn);
string field = $"{idxMicroStato}_{valoreIn}";
var searchVal = await _redisDb.HashGetAsync(currHash, field);
if (!searchVal.HasValue)
{
// ricarico tabella (salvando in redis x ricerca successiva)!
var valori = await StateMachInByKeyAsync(idxFamIn);
if (valori.Any(x => x.Key == field))
{
searchVal = valori.FirstOrDefault(x => x.Key == field).Value;
}
}
valOut = $"{searchVal}";
return valOut;
}
/// <summary>
/// cerca codice in anagrafica macchine ed eventualmente inserisce nuova macchina
/// </summary>
/// <param name="IdxMacchina"></param>
private async Task verificaIdxMacchinaAsync(string IdxMacchina)
{
// esecuzione in REDIS...cerco status macchina...
if ((await mDatiMacchineAsync(IdxMacchina)).Count == 0)
{
// verifico se esiste su DB
var dbRec = await _repo.MacchineGetByIdxAsync(IdxMacchina);
if (dbRec == null)
{
MacchineModel newRec = new MacchineModel()
{
IdxMacchina = IdxMacchina,
CodMacchina = "0000",
Nome = IdxMacchina,
Descrizione = "Macchina non codificata",
Note = "-",
locazione = "",
RecipeArchivePath = "",
RecipePath = ""
};
await _repo.MacchineUpsertAsync(newRec);
// verifico ci sia un microstato macchina...
var recMSM = await _repo.MicroStatoMacchinaGetByIdxMaccAsync(IdxMacchina);
if (recMSM.Count == 0)
{
// inserisco nuovo stato...
MicroStatoMacchinaModel msRec = new MicroStatoMacchinaModel()
{
IdxMacchina = IdxMacchina,
IdxMicroStato = 0,
InizioStato = DateTime.Now,
Value = "00"
};
await _repo.MicroStatoMacchinaUpsertAsync(msRec);
}
}
}
}
#endregion Private Methods
}
}
+15 -13
View File
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using MP.Core.DTO;
using MP.Core.Objects;
using MP.Data.DbModels;
using MP.Data.Services.IOC;
using MP.IOC.Data;
using Newtonsoft.Json;
using NLog;
@@ -16,17 +17,18 @@ namespace MP.IOC.Controllers
{
#region Public Constructors
public IOBController(IConfiguration configuration, MpDataService DataService)
public IOBController(IConfiguration configuration, MpDataService DataService, IIocService IService)
{
_configuration = configuration;
DService = DataService;
IOCService = IService;
}
#endregion Public Constructors
#region Public Methods
/// <summary>
/// <summary>
/// SALVA x macchina KVP parametro/valore:
/// GET: api/IOB/addOptPar/SIMUL_03?pName=PZREQ&pValue=1000
/// </summary> <param name="id"></param> <param name="pName"></param> <param name="pValue"></param>
@@ -327,15 +329,6 @@ namespace MP.IOC.Controllers
}
}
/// <summary>
/// Chiude ODL precedente ed avvia uno nuovo (duplicandolo e sitemando quantità RIMANENTE),
/// e CONFERMA produzione...
///
/// GET: IOB/forceSplitOdl/SIMUL_03
/// </summary>
/// <param name="id"></param>
/// <returns>Esito chiamata (OK/vuoto)</returns>
[HttpGet("forceSplitOdl/{id}")]
public async Task<IActionResult> ForceSplitOdl(string id)
{
@@ -358,6 +351,14 @@ namespace MP.IOC.Controllers
return Ok(answ);
}
/// <summary>
/// Chiude ODL precedente ed avvia uno nuovo (duplicandolo e sitemando quantità RIMANENTE),
/// e CONFERMA produzione...
///
/// GET: IOB/forceSplitOdl/SIMUL_03
/// </summary>
/// <param name="id"></param>
/// <returns>Esito chiamata (OK/vuoto)</returns>
/// <summary>
/// Recupera ArtNum dato CodXdl (per impianti che accettano solo INT in scrittura):
/// GET: IOB/getArtNum/SIMUL_03?CodXdl=ABC123
@@ -880,7 +881,8 @@ namespace MP.IOC.Controllers
DateTime dataOraEvento = DateTime.Now;
try
{
answ = await DService.ProcessInputAsync(id, valore, dtEve, dtCurr, cnt);
answ = await IOCService.ProcessInputAsync(id, valore, dtEve, dtCurr, cnt);
//answ = await DService.ProcessInputAsync(id, valore, dtEve, dtCurr, cnt);
return Ok(answ);
}
catch (Exception exc)
@@ -1447,8 +1449,8 @@ namespace MP.IOC.Controllers
#region Private Fields
private static IConfiguration _configuration = null!;
private static Logger Log = LogManager.GetCurrentClassLogger();
private IIocService IOCService;
#endregion Private Fields
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>8.16.2604.3016</Version>
<Version>8.16.2604.3017</Version>
</PropertyGroup>
<ItemGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 8.16.2604.3016</h4>
<h4>Versione: 8.16.2604.3017</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2604.3016
8.16.2604.3017
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2604.3016</version>
<version>8.16.2604.3017</version>
<url>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>