From 4bfe03962362abfb4cdee50fa9a5088587ad8a77 Mon Sep 17 00:00:00 2001 From: Samuele Locatelli Date: Wed, 23 Mar 2022 20:07:29 +0100 Subject: [PATCH] Update x gestione parametri con calcolo vario... --- MP.MONO.ADAPTER/conf/ParamList.json | 51 +++-- MP.MONO.Core/DTO/DisplayDataDTO.cs | 5 + MP.MONO.Core/Enums.cs | 8 +- MP.MONO.Core/VCData.cs | 48 +++++ MP.MONO.DECODER/AlarmsManager.cs | 33 ++-- MP.MONO.DECODER/ParamsManager.cs | 147 +++++++++++---- MP.MONO.DECODER/Program.cs | 62 ++---- MP.MONO.Data/Controllers/MpDbController.cs | 209 +++++++++++++++++---- MP.MONO.SIM/conf/ParamList.json | 25 ++- 9 files changed, 436 insertions(+), 152 deletions(-) create mode 100644 MP.MONO.Core/VCData.cs diff --git a/MP.MONO.ADAPTER/conf/ParamList.json b/MP.MONO.ADAPTER/conf/ParamList.json index 276461a..a5fa40e 100644 --- a/MP.MONO.ADAPTER/conf/ParamList.json +++ b/MP.MONO.ADAPTER/conf/ParamList.json @@ -1,6 +1,6 @@ [ { - "Order": 0, + "Order": 3, "Type": "SPEED-5000-10000", "Title": "SPEED", "Value": "4000", @@ -12,11 +12,11 @@ "EnablePlot": true, "ShowBar": true, "CssIcon": "fa-solid fa-gauge-high", - "SamplePeriod": 180 - + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { - "Order": 0, + "Order": 2, "Type": "FEED-3000-5000", "Title": "FEED", "Value": "2500", @@ -28,10 +28,11 @@ "EnablePlot": true, "ShowBar": true, "CssIcon": "fa-solid fa-gauge-high", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { - "Order": 0, + "Order": 1, "Type": "LOAD", "Title": "SPINDLE LOAD", "Value": "30", @@ -43,10 +44,11 @@ "EnablePlot": true, "ShowBar": true, "CssIcon": "fa-solid fa-bolt", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { - "Order": 0, + "Order": 4, "Type": "POS", "Title": "X POS", "Value": "1500", @@ -57,10 +59,11 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-ruler-horizontal", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { - "Order": 0, + "Order": 5, "Type": "POS", "Title": "Y POS", "Value": "5000", @@ -71,10 +74,11 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-ruler-horizontal", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { - "Order": 0, + "Order": 6, "Type": "POS", "Title": "Z POS", "Value": "-1500", @@ -85,10 +89,11 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-ruler-horizontal", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { - "Order": 0, + "Order": 7, "Type": "POS", "Title": "A POS", "Value": "150", @@ -99,6 +104,22 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-rotate-right", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" + }, + { + "Order": 8, + "Type": "POS", + "Title": "B POS", + "Value": "150", + "ValueNum": 150, + "MinVal": 0, + "MaxVal": 360, + "DisplFormat": "N3", + "IsNumeric": true, + "EnablePlot": true, + "CssIcon": "fa-solid fa-rotate-right", + "SamplePeriod": 180, + "VcFunc": "MEDIAN" } ] \ No newline at end of file diff --git a/MP.MONO.Core/DTO/DisplayDataDTO.cs b/MP.MONO.Core/DTO/DisplayDataDTO.cs index 73632d8..b86dc72 100644 --- a/MP.MONO.Core/DTO/DisplayDataDTO.cs +++ b/MP.MONO.Core/DTO/DisplayDataDTO.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using static MP.MONO.Core.Enums; // // This is here so CodeMaid doesn't reorganize this document @@ -67,5 +68,9 @@ namespace MP.MONO.Core.DTO /// Sample period for DB recording of min/Avg/MAX data (seconds) /// public double SamplePeriod { get; set; } = 60 * 3; + /// + /// Tipo di trasformazione VC da applicare ai dati al momento del raggiungimento del periodo di acquisizione + /// + public VC_func VcFunc { get; set; } = VC_func.POINT; } } diff --git a/MP.MONO.Core/Enums.cs b/MP.MONO.Core/Enums.cs index e853a7f..288a684 100644 --- a/MP.MONO.Core/Enums.cs +++ b/MP.MONO.Core/Enums.cs @@ -170,9 +170,9 @@ AVG, /// - /// Valore massimo del periodo + /// Calcolo MEDIANA /// - MAX, + MEDIAN, /// /// Valore minimo del periodo @@ -180,9 +180,9 @@ MIN, /// - /// Calcolo MEDIANA + /// Valore massimo del periodo /// - MEDIAN + MAX } #endregion Public Enums diff --git a/MP.MONO.Core/VCData.cs b/MP.MONO.Core/VCData.cs new file mode 100644 index 0000000..022deba --- /dev/null +++ b/MP.MONO.Core/VCData.cs @@ -0,0 +1,48 @@ +using static MP.MONO.Core.Enums; + +namespace MP.MONO.Core +{ + /// + /// Configurazione per Variabili Casuali + /// + public class VCData + { + #region Public Fields + + /// + /// DataOra inizio periodo di elaborazione x determinare periodo massimo + /// + public DateTime DTStart; + + #endregion Public Fields + + #region Public Properties + + /// + /// Array dati per calcolo + /// + public List dataArray { get; set; } = new List(); + + /// + /// Tipologia di funzione da applicare + /// + public VC_func Funzione { get; set; } = VC_func.POINT; + + /// + /// Periodo di riferimento in secondi (da aggiungere a DTStart x determinare scadenza x elaborazione) + /// + public double Period { get; set; } = 60.0; + + #endregion Public Properties + + public bool isElapsed + { + get + { + bool answ = false; + answ = DTStart.AddSeconds(Period) <= DateTime.Now; + return answ; + } + } + } +} \ No newline at end of file diff --git a/MP.MONO.DECODER/AlarmsManager.cs b/MP.MONO.DECODER/AlarmsManager.cs index 2721bc5..95a7884 100644 --- a/MP.MONO.DECODER/AlarmsManager.cs +++ b/MP.MONO.DECODER/AlarmsManager.cs @@ -1,21 +1,26 @@ using NLog; using NLua; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace MP.MONO.DECODER { public class AlarmsManager { - protected List AlarmList = new List(); - protected static Lua state = new Lua(); + #region Protected Fields + + protected static Logger Log = LogManager.GetCurrentClassLogger(); protected static string luaPath = ""; + protected static Lua state = new Lua(); + + #endregion Protected Fields + + #region Public Fields + public static string alarmStatus = ""; public static bool valueChanged = false; - protected static Logger Log = LogManager.GetCurrentClassLogger(); + + #endregion Public Fields + + #region Public Constructors public AlarmsManager() { @@ -26,16 +31,20 @@ namespace MP.MONO.DECODER state.DoString(" callMode = 'NLua' "); // initi 0/0 state.DoString(" lastVal = 0 "); - state.DoString(" currVal = 0 "); + state.DoString(" currVal = 0 "); #endif state["callMode"] = "NLua"; state["lastVal"] = 0; - state["currVal"] = 0; + state["currVal"] = 0; Log.Info("AlarmsManager OK"); Console.WriteLine("AlarmsManager OK"); } + #endregion Public Constructors + + #region Public Methods + /// /// Funzione chiamata LUA x calcolo status allarmi /// @@ -78,5 +87,7 @@ namespace MP.MONO.DECODER } return answ; } + + #endregion Public Methods } -} +} \ No newline at end of file diff --git a/MP.MONO.DECODER/ParamsManager.cs b/MP.MONO.DECODER/ParamsManager.cs index 7a34331..e304619 100644 --- a/MP.MONO.DECODER/ParamsManager.cs +++ b/MP.MONO.DECODER/ParamsManager.cs @@ -1,60 +1,113 @@ -using NLog; +using MP.MONO.Core; +using MP.MONO.Core.DTO; +using MP.MONO.Data.DbModels; +using NLog; using NLua; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace MP.MONO.DECODER { public class ParamsManager { - protected List AlarmList = new List(); - protected static Lua state = new Lua(); + #region Private Fields + + /// + /// Dizionario dei valori accumulati sulle variabili + /// + private Dictionary ParamsAccumulator = new Dictionary(); + + #endregion Private Fields + + #region Protected Fields + + protected static Logger Log = LogManager.GetCurrentClassLogger(); protected static string luaPath = ""; + protected static Lua state = new Lua(); + protected List AlarmList = new List(); + + #endregion Protected Fields + + #region Public Fields + public static string alarmStatus = ""; public static bool valueChanged = false; - protected static Logger Log = LogManager.GetCurrentClassLogger(); + + #endregion Public Fields + + #region Public Constructors public ParamsManager() { // preparo variabile x avvisare script che il modo è da NLua luaPath = Path.Combine(Directory.GetCurrentDirectory(), "lua", "ParamsDecoder.lua"); -#if false - state.DoString(" callMode = 'NLua' "); - // initi 0/0 - state.DoString(" lastVal = 0 "); - state.DoString(" currVal = 0 "); -#endif state["callMode"] = "NLua"; state["lastVal"] = 0; - state["currVal"] = 0; + state["currVal"] = 0; Log.Info("ParamsManager OK"); Console.WriteLine("ParamsManager OK"); } - /// - /// Funzione chiamata LUA x calcolo status allarmi - /// - /// - /// - /// - /// - /// - /// - /// - public static List processData(string prefix, List alarmList, uint muteMask, uint disableMask, uint lastVal, ref uint currVal) - { - state["prefix"] = prefix; - state["alarmList"] = alarmList; - state["numAlarm"] = alarmList.Count; - state["muteMask"] = muteMask; - state["disableMask"] = disableMask; + #endregion Public Constructors + + #region Public Methods + + /// + /// Funzione chiamata LUA x calcolo degli eventuali parametri con periodi "scaduti" + /// + /// + /// + /// + public List processData(Dictionary paramsList, List redisParamConf) + { + List answ = new List(); + DateTime adesso = DateTime.Now; + + // vado ad "accumulare i dati" a quelli presenti... + foreach (var item in paramsList) + { + var currParam = redisParamConf.FirstOrDefault(x => x.Title == item.Key); + // cerco nelle variabili accomulatori... + if (!ParamsAccumulator.ContainsKey(item.Key)) + { + if (currParam != null) + { + var dataList = new List(); + dataList.Add(item.Value); + VCData newSet = new VCData() + { + dataArray = dataList, + DTStart = adesso, + Funzione = currParam.VcFunc, + Period = currParam.SamplePeriod + }; + // se non ci fosse creo + ParamsAccumulator.Add(item.Key, newSet); + } + } + else + { + // altrimenti aggiungo + ParamsAccumulator[item.Key].dataArray.Add(item.Value); + } + + // effettuo verifiche scadenza + if (ParamsAccumulator.ContainsKey(item.Key)) + { + if (ParamsAccumulator[item.Key].isElapsed) + { + // se scaduto --> mando a LUA x calcolo + + // FIXME TODO + var calcVal = ParamsAccumulator[item.Key].dataArray.FirstOrDefault(); +#if false + // processing LUA del calcolo dei dati "scaduti" + //state["prefix"] = prefix; + //state["alarmList"] = alarmList; + //state["numAlarm"] = alarmList.Count; + //state["muteMask"] = muteMask; + //state["disableMask"] = disableMask; - List answ = new List(); // invio dati aggiornati a LUA state["lastVal"] = lastVal; state["currVal"] = currVal; @@ -76,7 +129,31 @@ namespace MP.MONO.DECODER answ.Add($"{allarme}"); } } +#endif + + + // aggiungo alla lista finale... + var dbRecord = new DataLogModel() + { + DtRif = adesso, + FluxType = item.Key, + MachineId = 1, + ValNum = calcVal, + ValStr = $"{calcVal:N3}" + + }; + answ.Add(dbRecord); + + // elimino dai valori accumulati... + ParamsAccumulator.Remove(item.Key); + } + } + } + + return answ; } + + #endregion Public Methods } -} +} \ No newline at end of file diff --git a/MP.MONO.DECODER/Program.cs b/MP.MONO.DECODER/Program.cs index 468f4a4..5b1e091 100644 --- a/MP.MONO.DECODER/Program.cs +++ b/MP.MONO.DECODER/Program.cs @@ -202,11 +202,16 @@ void AlarmsValPipe_EA_NewMessage(object? sender, EventArgs e) alarmsPipe.saveAndSendMessage(Constants.ALARM_CURR_KEY, serAlarms); if (dbController != null) { + // salvo sul DB + _ = dbController.AlarmLogInsertMany(alarmLogList).Result; + +#if false // salvo nel DB lo stato dei bank di allarme foreach (var dbItem in alarmLogList) { _ = dbController.AlarmLogInsert(dbItem).Result; - } + } +#endif } } } @@ -227,37 +232,8 @@ void ParamsValPipe_EA_NewMessage(object? sender, EventArgs e) { try { - // esempio conversione dati -#if false - List answ = new List(); - - // genero a partire dall'elenco configurato da simulare... - answ = currSimPar.Select(i => new DisplayDataDTO() - { - IsNumeric = i.IsNumeric, - MaxVal = i.MaxVal, - MinVal = i.MinVal, - Order = i.Order, - Title = i.Title, - Type = i.Type, - ValueNum = simNext(i.ValueNum, i.MinVal, i.MaxVal, i.SimMean, i.SimStd), - DisplFormat = i.DisplFormat, - CssIcon = i.CssIcon, - EnablePlot = i.EnablePlot, - ShowBar = i.ShowBar, - ShowGauge = i.ShowGauge, - }).ToList(); - - foreach (var item in answ) - { - item.Value = $"{item.ValueNum.ToString(item.DisplFormat)}"; - } - return answ; -#endif - // verifico codici ricevuti var paramsList = JsonConvert.DeserializeObject>(currArgs.newMessage); - if (paramsList != null) { // recupero elenco parametri salvati in redis... @@ -283,26 +259,22 @@ void ParamsValPipe_EA_NewMessage(object? sender, EventArgs e) updatedParams.Add(currParam); } } + if (dbController != null) + { + // verifico x ogni parametro se sia completato il periodo di campionamento... + var params2save = paramMan.processData(paramsList, redisParamConf); + if (params2save.Count > 0) + { + // salvo sul DB + _ = dbController.DataLogInsertMany(params2save).Result; + } + } } } - //// salvo status aggiornato parametri... - //redisDb.StringSet(Constants.PARAMS_CURR_KEY, JsonConvert.SerializeObject(redisParamConf)); - - string updRawVal = JsonConvert.SerializeObject(redisParamConf); - // invio sulla message pipeline corretta TUTTI i parametri aggiornati serializzati + string updRawVal = JsonConvert.SerializeObject(redisParamConf); paramsPipe.saveAndSendMessage(Constants.PARAMS_CURR_KEY, updRawVal); - - if (dbController != null) - { - // effettuo update in DB se ho - //// salvo nel DB lo stato dei bank di allarme - //foreach (var dbItem in alarmLogList) - //{ - // _ = dbController.AlarmLogInsert(dbItem).Result; - //} - } } } catch (Exception exc) diff --git a/MP.MONO.Data/Controllers/MpDbController.cs b/MP.MONO.Data/Controllers/MpDbController.cs index 4bda399..83aba20 100644 --- a/MP.MONO.Data/Controllers/MpDbController.cs +++ b/MP.MONO.Data/Controllers/MpDbController.cs @@ -1,29 +1,26 @@ -using Microsoft.Extensions.Configuration; -using MP.MONO.Core; +using Microsoft.EntityFrameworkCore; using MP.MONO.Core.DTO; using MP.MONO.Data.DbModels; using NLog; -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace MP.MONO.Data.Controllers { public class MpDbController : IDisposable { + #region Private Fields + private static NLog.Logger Log = LogManager.GetCurrentClassLogger(); + #endregion Private Fields + + #region Public Constructors + public MpDbController() { } - public void Dispose() - { - // Clear database context - //Log.Info("Dispose di GWMSController"); - } + #endregion Public Constructors + + #region Public Methods /// /// Recupero elenco allarmi (ultimi datop "skip") @@ -82,27 +79,150 @@ namespace MP.MONO.Data.Controllers return fatto; } - public ProductionDTO MachineGetProd() + /// + /// Inserimento di un record AlarmLog + /// + /// Lista Record da inserire (senza ID...) + /// + public async Task AlarmLogInsertMany(List newItems) { - // !!!FIXME TODO... è fake... - - Random rand = new Random(); - int stdCycle = 5; - - ProductionDTO currMachDto = new ProductionDTO() + bool fatto = false; + using (MapoMonoContext localDbCtx = new MapoMonoContext()) { - Order = "ODL Test", - ItemCode = "ART.0000123", - ProgName = "P000012", - CurrQty = DateTime.Now.Minute + rand.Next(1, 40), - OrderQty = 100, - CycleTimeMin = rand.NextDouble() * stdCycle, - Message = "...simulated data..." - }; + try + { + await localDbCtx + .DbSetAlarmLog + .AddRangeAsync(newItems); + await localDbCtx.SaveChangesAsync(); + fatto = true; + } + catch (Exception exc) + { + Log.Error($"Eccezione durante AlarmLogInsert{Environment.NewLine}{exc}"); + } + } + return fatto; + } - Task.Delay(400).Wait(); + /// + /// Recupero record DataLog data condizione filtro + /// + /// + /// + /// + /// + /// + public List DataLogGetFilt(int MachineId, string FluxType, int skipRec, int numRec) + { + List dbResult = new List(); + using (MapoMonoContext localDbCtx = new MapoMonoContext()) + { + try + { + dbResult = localDbCtx + .DbSetDataLog + .Where(x => x.MachineId == MachineId && x.FluxType == FluxType) + .Include(m => m.MachineNav) + .OrderByDescending(x => x.DataLogId) + .Skip(skipRec) + .Take(numRec) + .ToList(); + } + catch (Exception exc) + { + Log.Error($"Eccezione durante DataLogGetFilt{Environment.NewLine}{exc}"); + } + } + return dbResult; + } - return currMachDto; + /// + /// Inserimento di un SET di record DataLog (post aggregazione base VC) + /// + /// Lista Record da inserire (senza ID...) + /// + public async Task DataLogInsertMany(List newItems) + { + bool fatto = false; + using (MapoMonoContext localDbCtx = new MapoMonoContext()) + { + try + { + await localDbCtx + .DbSetDataLog + .AddRangeAsync(newItems); + await localDbCtx.SaveChangesAsync(); + fatto = true; + } + catch (Exception exc) + { + Log.Error($"Eccezione durante DataLogInsert{Environment.NewLine}{exc}"); + } + } + return fatto; + } + /// + /// Recupero record DataStAg (aggregati) data condizione filtro + /// + /// + /// + /// + /// + /// + public List DataStAgGetFilt(int MachineId, string FluxType, int skipRec, int numRec) + { + List dbResult = new List(); + using (MapoMonoContext localDbCtx = new MapoMonoContext()) + { + try + { + dbResult = localDbCtx + .DbSetDataStAg + .Where(x => x.MachineId == MachineId && x.FluxType == FluxType) + .Include(m => m.MachineNav) + .OrderByDescending(x => x.DataStAgId) + .Skip(skipRec) + .Take(numRec) + .ToList(); + } + catch (Exception exc) + { + Log.Error($"Eccezione durante DataLogGetFilt{Environment.NewLine}{exc}"); + } + } + return dbResult; + } + /// + /// Inserimento di un record DataStAg (aggregati) - creati da ANALYZER che effettua compattazione + /// + /// Record da inserire (senza ID...) + /// + public async Task DataStAgInsert(DataStAgModel newItem) + { + bool fatto = false; + using (MapoMonoContext localDbCtx = new MapoMonoContext()) + { + try + { + localDbCtx + .DbSetDataStAg + .Add(newItem); + await localDbCtx.SaveChangesAsync(); + fatto = true; + } + catch (Exception exc) + { + Log.Error($"Eccezione durante DataStAgInsert{Environment.NewLine}{exc}"); + } + } + return fatto; + } + + public void Dispose() + { + // Clear database context + //Log.Info("Dispose di GWMSController"); } public List MachineGetDisplay() @@ -185,11 +305,34 @@ namespace MP.MONO.Data.Controllers }; answ.Add(displ06); - Task.Delay(200).Wait(); return answ; } - } -} + public ProductionDTO MachineGetProd() + { + // !!!FIXME TODO... è fake... + + Random rand = new Random(); + int stdCycle = 5; + + ProductionDTO currMachDto = new ProductionDTO() + { + Order = "ODL Test", + ItemCode = "ART.0000123", + ProgName = "P000012", + CurrQty = DateTime.Now.Minute + rand.Next(1, 40), + OrderQty = 100, + CycleTimeMin = rand.NextDouble() * stdCycle, + Message = "...simulated data..." + }; + + Task.Delay(400).Wait(); + + return currMachDto; + } + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/MP.MONO.SIM/conf/ParamList.json b/MP.MONO.SIM/conf/ParamList.json index ca28a3f..a5fa40e 100644 --- a/MP.MONO.SIM/conf/ParamList.json +++ b/MP.MONO.SIM/conf/ParamList.json @@ -12,8 +12,8 @@ "EnablePlot": true, "ShowBar": true, "CssIcon": "fa-solid fa-gauge-high", - "SamplePeriod": 180 - + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 2, @@ -28,7 +28,8 @@ "EnablePlot": true, "ShowBar": true, "CssIcon": "fa-solid fa-gauge-high", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 1, @@ -43,7 +44,8 @@ "EnablePlot": true, "ShowBar": true, "CssIcon": "fa-solid fa-bolt", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 4, @@ -57,7 +59,8 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-ruler-horizontal", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 5, @@ -71,7 +74,8 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-ruler-horizontal", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 6, @@ -85,7 +89,8 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-ruler-horizontal", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 7, @@ -99,7 +104,8 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-rotate-right", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" }, { "Order": 8, @@ -113,6 +119,7 @@ "IsNumeric": true, "EnablePlot": true, "CssIcon": "fa-solid fa-rotate-right", - "SamplePeriod": 180 + "SamplePeriod": 180, + "VcFunc": "MEDIAN" } ] \ No newline at end of file