/// /// Copyright (c) 2018-2023 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.Net; using log4net; using FluentNHibernate.Cfg; using FluentNHibernate.Cfg.Db; using NHibernate; using NHibernate.Criterion; using Common; using TracingDB.Entities; namespace TBF.Rig.Output.DB.ProductionTracing { public enum Retv { OK, Error, } class DateTimeComparer : IComparer { public int Compare(DateTime x, DateTime y) { return DateTime.Compare(x, y); } } public class Tracing : ComponentBase, IOperation, GenericDevices.IResultsWriter, GenericDevices.IStartInfoReader, Generic.IDevice { private static readonly ILog log = LogManager.GetLogger(typeof(Tracing)); public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } public const string WorkstepName = "Test_bench"; public const int WorkstepRsltOK = 0; MonitoringCfg tracingCfg; string workplace { get { TBF.Rig.GenericDevices.IBenchInfo benchInfo = TBF.Rig.Sequences.ProcessData.BenchInfo; return (benchInfo != null) ? benchInfo.TestBenchName : "TestBench"; } } IPAddress ipAddress; IPAddress netMask; public IPAddress IPAddress { get { return ipAddress; } } public IPAddress NetMask { get { return netMask; } } enum OpState { None, CheckPreviousRecordsScheduled, CheckPreviousRecordsRunning, SaveTracingRecordsScheduled, SaveTracingRecordsRunning, } /// OpState currentOpState; bool opCompleted; bool anyError; /// /// Watermeters to check at the beginning of the cycle /// IList waterMeters; /// /// Data (tracing records) to write at the end of cycle /// Results.Entities.Batch batch; ISessionFactory sessionFactory; /// Factory to create database sessions that is initialized in the constructor IList processes; Dictionary processDictionary; Dictionary> workstepsDictionary; /// /// Previous workstep verification info /// Process currentProcess; /// Currently used process Workstep currentWorkstep; /// Currently used workstep of this test bench Workstep verifiedWorkstep; Part verifiedPart; /// Applicable when verifyReferencePart == false bool verifyReferencePart; int CyclesToSkip; public Tracing() {} public Tracing(Generic.IComponentCfg cfg) : base(cfg) { tracingCfg = cfg as MonitoringCfg; } public override void Initialize() { Network.AdapterInfo.RefreshNetAdaptersInfo(); Network.AdapterInfo netadapter = Network.AdapterInfo.GetNetAdapter(tracingCfg.NetAdapter); ipAddress = netadapter.IPAddress; netMask = netadapter.NetMask; currentProcess = null; currentOpState = OpState.None; if (tracingCfg.DebugLevel == DebugMode.Normal) { /// Session factory is used to create database sessions sessionFactory = Fluently.Configure() .Database(MySQLConfiguration.Standard.ConnectionString(tracingCfg.ConnStr)) .Mappings(m => m.FluentMappings.AddFromAssemblyOf()) .ExposeConfiguration(TracingDB.DB.BuildSchema) .BuildConfiguration() .SetProperty("hibernate.connection.connect_timeout", Const.MySqlConnectTimeoutSec) .BuildSessionFactory(); /// Register this test bench in the tracing DB for approx. 2 weeks var session = sessionFactory.OpenSession(); #if !DEBUG /// Only a release version registers a workplace TracingDB.DB.RegisterWorkplace(session, workplace, Users.CurrentUser.UserName(), (ipAddress != null) ? ipAddress.ToString() : "1.2.3.4", "", WorkstepName, DateTime.Now + new TimeSpan(15, 0, 0, 0)); session.Flush(); #endif session.Close(); TracingDB.DB.SessionFactory = sessionFactory; CyclesToSkip = 0; log.FatalFormat("{0} initialized: {1}", Name, this); } else { log.FatalFormat("{0} simulated: {1}", Name, this); } } #region Configuration Change Handling public static void OnDataChange(object sender, DataChangeArgs args) { if (DataChangeHandler == null) return; try { DataChangeHandler(sender, args); } catch (Exception e) { log.Error("CfgChangeHandler(...) failed", e); } } public static event EventHandler DataChangeHandler; public override void StartChangeHandler() { DataChangeHandler += delegate (object sender, DataChangeArgs args) { var intbox = args.Data as Boxes.IntBox; if (args.Command == CfgChangeCmd.SetData && intbox != null) { CyclesToSkip = intbox.Val; } else if (args.Command == CfgChangeCmd.GetData) { TracingCfgCtrl.OnCmdResponse(this, new CmdResponseArgs(CfgChangeCmd.GetData, 0, CyclesToSkip)); } }; } #endregion Configuration Change Handling /// /// IDevice interface implementation /// public void RunDeviceBefore() { } public void RunDeviceAfter() { } /// public void StopDevice() { if (tracingCfg.DebugLevel != DebugMode.Normal) return; using (ISession session = sessionFactory.OpenSession()) { TracingDB.DB.UnregisterWorkplaceObsolete(session, workplace); session.Flush(); } } public void StopDevice2() {} /// /// Reads information on start of a cycle, Events: Event.InfoRead /// /// Results of water meters /// Reference to the operation public IOperation ReadStartInfoOp(IList waterMeters) { if (!tracingCfg.CheckPreviousRecords) { return null; } else if ((currentOpState == OpState.CheckPreviousRecordsRunning) || (currentOpState == OpState.SaveTracingRecordsRunning)) { throw new Exception("Sequence error"); } else { this.waterMeters = waterMeters; currentOpState = OpState.CheckPreviousRecordsScheduled; return this; } } /// /// Writes the test cycle results into a file, Events: Event.ResultsWritten /// /// Procedure to print the results of /// Results to write into the file /// Reference to the operation public IOperation ProcessResultsOp(Results.Entities.Batch batch) { if (!tracingCfg.SaveTracingRecords) { return null; } else if ((currentOpState == OpState.CheckPreviousRecordsRunning) || (currentOpState == OpState.SaveTracingRecordsRunning)) { throw new Exception("Sequence error"); } else { this.batch = batch; currentOpState = OpState.SaveTracingRecordsScheduled; return this; } } /// Start this operation public void Start() { if (currentOpState == OpState.CheckPreviousRecordsScheduled) { currentOpState = OpState.CheckPreviousRecordsRunning; } else if (currentOpState == OpState.SaveTracingRecordsScheduled) { currentOpState = OpState.SaveTracingRecordsRunning; } opCompleted = false; anyError = false; } /// Run this operation /// Event.ResultsWritten or Event.Error public Event Run() { if (currentOpState == OpState.CheckPreviousRecordsRunning) { if (tracingCfg.DebugLevel == DebugMode.Simulate) { return Event.InfoRead; } else if (!opCompleted) { log.WarnFormat("{0} : Run() : currentOp = {1}", Name, currentOpState); opCompleted = true; if (CyclesToSkip > 0) { CyclesToSkip--; } else if (CheckPreviousRecords(waterMeters) != Retv.OK) { anyError = true; } return anyError ? Event.InfoNotRead : Event.InfoRead; } else { return anyError ? Event.InfoNotRead : Event.InfoRead; } } else if (currentOpState == OpState.SaveTracingRecordsRunning) { if (tracingCfg.DebugLevel == DebugMode.Simulate || batch.WaterMeters.Count == 0) { return Event.ResultsWritten; } else if (!opCompleted) { log.WarnFormat("{0} : Run() : currentOp = {1}", Name, currentOpState); opCompleted = true; if (SaveTracingRecords(batch) != Retv.OK) anyError = true; if (TBF.DB.EventsDBSessionFactory != null && anyError) { NHibernate.ISession session = null; try { session = TBF.DB.EventsDBSessionFactory.OpenSession(); Events.DB.LoadSubscribers(session); TBF.UiBridge.Bridge.TriggerEvent(session, Name, EventClass.ComputerResources, Severity.Error, string.Format("Nepodarilo sa uložiť výsledky do sledovacej DB: Číslo dávky={0}", batch.BatchNr), string.Format("Nepodarilo sa uložiť výsledky do sledovacej DB\r\nČíslo dávky = {0}", batch.BatchNr), SubscriberGroup.Metrology | SubscriberGroup.Maintenance | SubscriberGroup.Production); } catch (Exception exc) { log.ErrorFormat("Triggering event(s) failed: {0}", exc.Message); } finally { if (session != null && session.IsOpen) session.Close(); } } return anyError ? Event.ErrorProcessingResults : Event.ResultsWritten; } else { if (TBF.DB.EventsDBSessionFactory != null && anyError) { NHibernate.ISession session = null; try { session = TBF.DB.EventsDBSessionFactory.OpenSession(); Events.DB.LoadSubscribers(session); TBF.UiBridge.Bridge.TriggerEvent(session, Name, EventClass.ComputerResources, Severity.Error, string.Format("Nepodarilo sa uložiť výsledky do sledovacej DB: Číslo dávky={0}", batch.BatchNr), string.Format("Nepodarilo sa uložiť výsledky do sledovacej DB\r\nČíslo dávky = {0}", batch.BatchNr), SubscriberGroup.Metrology | SubscriberGroup.Maintenance | SubscriberGroup.Production); } catch (Exception exc) { log.ErrorFormat("Triggering event(s) failed: {0}", exc.Message); } finally { if (session != null && session.IsOpen) session.Close(); } } return anyError ? Event.ErrorProcessingResults : Event.ResultsWritten; } } else { return Event.None; } } /// Stop this operation public void Stop() { currentOpState = OpState.None; } Retv CheckPreviousRecords(IList waterMeters) { Retv retVal = Retv.Error; bool isAtLeastOneNOK = false; using (ISession session = sessionFactory.OpenSession()) { for (int i = 0; i < waterMeters.Count; i++) { Results.Entities.WaterMeter wm = waterMeters[i]; if ((wm != null) && !wm.Disabled) { CheckPreviousRecordOfSingleWM(session, wm); if (wm.LastRecordIsNok) { isAtLeastOneNOK = true; } } } retVal = Retv.OK; } return retVal; } Retv CheckPreviousRecordOfSingleWM(ISession session, Results.Entities.WaterMeter wm) { if (string.IsNullOrEmpty(wm.SerialNr)) { /// S/N is missing wm.ProcessId = 0; wm.LastRecordIsNok = false; /// OK, this is an RFID comm. error, not a production tracing error return Retv.OK; } /// /// Get all already existing reference records with Code == wm.SerialNr, refRecords[0] will be the most recent one /// ReferenceRecord refRecord = null; Workstep workstep = null; ProjectionList projections = Projections.ProjectionList(); projections.Add(Projections.Property(() => refRecord.Id)); /// rcrd[0] = reference record ID projections.Add(Projections.Property(() => workstep.Id)); /// rcrd[1] = workstep ID projections.Add(Projections.Property(() => refRecord.Process.Id)); /// rcrd[2] = process ID projections.Add(Projections.Property(() => refRecord.TimeStamp)); /// rcrd[3] = reference record time stamp projections.Add(Projections.Property(() => refRecord.Result)); /// rcrd[4] = result: =0...OK, >0...error code /// /// Get all reference records with Code == SerialNr from all worksteps different from 'Test_bench' /// IList records = session.QueryOver(() => refRecord) .Where(rr => (rr.Code == wm.SerialNr)) .OrderBy(rr => rr.TimeStamp).Asc .JoinQueryOver(rr => rr.Workstep, () => workstep) .And(ws => (ws.Name != WorkstepName)) .Select(projections) .List(); if (records.Count == 0) { /// No records found wm.ProcessId = 0; wm.LastRecordIsNok = true; /// NOK /// return Retv.OK; } /// /// Get processId of the last record /// int lastProcessId = (int)records[records.Count - 1][2]; DateTime lastTimeStamp = (DateTime)records[records.Count - 1][3]; if (lastProcessId == 0) return Retv.Error; /// This should never happen if ((currentProcess == null) || (currentProcess.Id != lastProcessId)) { /// /// Process changed ==> update currentProcess / currentWorkstep / verifiedWorkstep / verifiedPart / verifyReferencePart /// var processes = session.QueryOver() .Where(p => (p.Id == lastProcessId)) .List(); if (processes.Count != 1) return Retv.Error; IList worksteps = session.QueryOver() .Where(ws => (ws.Process == processes[0])) .And( ws => (ws.Name == WorkstepName)) .List(); if (worksteps.Count != 1) return Retv.Error; TracingDB.ScanVerificationInfo info; /// if (null == (info = TracingDB.DB.AnalyzeProcess(session, processes[0], worksteps[0]))) { return Retv.Error; /// Unable to } /// currentProcess = processes[0]; currentWorkstep = worksteps[0]; verifiedWorkstep = info.Workstep; verifiedPart = info.Part; verifyReferencePart = info.VerifyReferencePart; } wm.ProcessId = currentProcess.Id; wm.LastRecordIsNok = true; /// NOK (=default if no matching record found) foreach (var rcrd in records) { if ((verifiedWorkstep.Id == (int)rcrd[1]) && (lastProcessId == (int)rcrd[2])) { /// This record fits verification requirements. /// As records are ordered by time, the result of the last one determines the verification result. wm.LastRecordIsNok = ((int)rcrd[4] != WorkstepRsltOK); } } return Retv.OK; } /// /// Write results of a batch of water meters to the DB /// /// DB session /// Batch entity public Retv SaveTracingRecords(Results.Entities.Batch batch) { ITransaction transaction = null; ISession session = null; /// /// Save to regular tracing DB /// try { session = sessionFactory.OpenSession(); transaction = session.BeginTransaction(); int writtenRecordsCount = 0; foreach (var wMtr in batch.WaterMeters) { writtenRecordsCount += SaveSingleWM2DB(session, wMtr); } transaction.Commit(); session.Flush(); log.ErrorFormat("Batch {0}: {1} of {2} watermeters written to the regular production tracing DB", batch.BatchNr, writtenRecordsCount, batch.WaterMeters.Count); } catch (Exception exc) { if (transaction != null && !transaction.WasCommitted) transaction.Rollback(); log.WarnFormat("Failed to write results of batch {0} to production tracing DB: {1}", batch.BatchNr, exc.Message); return Retv.Error; } finally { if (session != null) { session.Close(); session.Dispose(); } } return Retv.OK; } /// /// Write one meter results to the DB /// /// DB session /// Watermeter entity /// Number of written reference record int SaveSingleWM2DB(ISession session, Results.Entities.WaterMeter wm) { if (string.IsNullOrEmpty(wm.SerialNr)) { /// Without PCB number there is no DB activity return 0; } else if (wm.ProcessId != 0 && currentProcess != null && wm.ProcessId == currentProcess.Id) { /// Process is already loaded => save a reference record for this WM session.SaveOrUpdate(new ReferenceRecord(currentProcess, currentWorkstep, wm.SerialNr, Users.CurrentUser.UserName(), workplace, wm.Passed ? 0 : 1)); return 1; } else if (wm.ProcessId != 0) { /// Process ID is already known, however process and workstep have to be loaded IList processes = session.QueryOver() .Where(x => (x.Id == wm.ProcessId)) .List(); if (processes.Count != 1) return 0; foreach (var ws in processes[0].Worksteps) { if (ws.Name == WorkstepName) { session.SaveOrUpdate(new ReferenceRecord(processes[0], ws, wm.SerialNr, Users.CurrentUser.UserName(), workplace, wm.Passed ? 0 : 1)); return 1; } } return 0; /// No workste with 'WorkstepName' found } else { /// /// Process ID is unknown => do everything from scratch /// /// Get all already existing reference records with Code == wm.SerialNr, refRecords[0] will be the most recent one ReadProcessesFromDB(session); var refRecords = session.QueryOver() .Where(rr => (rr.Code == wm.SerialNr)) .OrderBy(rr => rr.TimeStamp).Desc .List(); /// Save a new reference record from this test bench if workstep "Test_bench" is defined in the obtained WM process IList worksteps; if (refRecords.Count > 0 && workstepsDictionary.TryGetValue(refRecords[0].Process.Id, out worksteps) && worksteps != null && worksteps.Count == 1) { session.SaveOrUpdate(new ReferenceRecord(refRecords[0].Process, worksteps[0], wm.SerialNr, Users.CurrentUser.UserName(), workplace, wm.Passed ? 0 : 1)); return 1; } return 0; } } /// /// Updates processes, processDictionary and workstepsDictionary /// /// DB session void ReadProcessesFromDB(ISession session) { processes = session.QueryOver() .List(); processDictionary = new Dictionary(); workstepsDictionary = new Dictionary>(); /// foreach (var pr in processes) { IList worksteps = session.QueryOver() .Where(ws => ((ws.Process == pr) && (ws.Name == WorkstepName))) .List(); processDictionary.Add(pr.Id, pr); workstepsDictionary.Add(pr.Id, worksteps); } } } }