/// /// Copyright (c) 2013-2015 Sensus Metering Systems /// using System; using System.Text; using System.Threading; using System.Diagnostics; using System.Collections.Generic; using System.Windows.Forms; using log4net; using NHibernate; using TBF.BenchControl.Generic; using TBF.BenchControl.GenericDevices; using TBF.BenchControl.Sequences; namespace TBF.BenchControl { public class QuitStateMachineException : Exception { } /// /// This class controls real test bench behavior. /// It is based on a state machine /// public static class StateMachine { private static readonly ILog log = LogManager.GetLogger(typeof(StateMachine)); private static readonly ILog wlog = LogManager.GetLogger(typeof(StateMachine)); /// Private devices and components static IList components; /// list of all components static IList devices; /// list of devices /// /// Bench paths static IList feedingPaths; static IList benchPaths; static IList outputPaths; static IList metersPaths; public static IList TransitionSequences; /// Public components public static Elde.ControlBoardDev ControlBoard; public static GenericDevices.IAmbient Ambient; public static GenericDevices.IBalance Balance1; public static GenericDevices.IBalance Balance2; public static GenericDevices.IBalance Balance3; public static GenericDevices.IValve EmptyTankValve1; public static GenericDevices.IValve EmptyTankValve2; public static GenericDevices.IValve EmptyTankValve3; public static IList MasterValves; /// list of directly controlled valves public static IList CoupledValves; /// list of coupled valves public static IList ExtendedValves; /// list of extended valves /// Time and synchronization #if IPERLST public const int Period = 2; /// State machine period in sec. #else public const int Period = 1; /// State machine period in sec. #endif static DateTime startDateTime; /// DateTime of time instance when the state machine worker thread starts static int currentTimeSec; /// Time from the start of the state machine in seconds static bool quitStateMachine; /// flag to stop the worker thread /// true when the state machine is running static bool stateMachineRunning; public static bool Running { get { return stateMachineRunning; } } /// Worker thread and database session static Thread workerThread; public static ISession WtSession; static IList states; /// list of states public static IList DefaultValvesOpen { get { return GenericDevices.ValveBase.Merge( Utils.ValvesOpen((feedingPaths != null && feedingPaths.Count > 0) ? feedingPaths[0] : null), Utils.ValvesOpen((benchPaths != null && benchPaths.Count > 0) ? benchPaths[0] : null), Utils.ValvesOpen((outputPaths != null && outputPaths.Count > 0) ? outputPaths[0] : null) ); } } public static IList DefaultValvesClose { get { return GenericDevices.ValveBase.Merge( Utils.ValvesClose((feedingPaths != null && feedingPaths.Count > 0) ? feedingPaths[0] : null), Utils.ValvesClose((benchPaths != null && benchPaths.Count > 0) ? benchPaths[0] : null), Utils.ValvesClose((outputPaths != null && outputPaths.Count > 0) ? outputPaths[0] : null) ); } } /// /// Loaded by LoadProcedure() or IOperation LoadProcedureOp(...) /// public static Entities.Procedure Procedure; /// Procedure public static IList Tests; /// Tests /// Hardware devices connected to the PC controlling the bench. public static IList Components { get { return components; } } public static IList Devices { get { return devices; } } /// /// DateTime of time instance when the state machine worker thread starts /// public static DateTime StartDateTime { get { return startDateTime; } } /// /// Current state name /// public static int Time { get { return currentTimeSec; } } /// /// Constructor /// static StateMachine() { devices = new List(); states = new List(); currentTimeSec = 0; quitStateMachine = false; } /// /// Add a device to the state machine. /// /// Device public static void AddDevice(IDevice device) { if (device != null && !devices.Contains(device)) devices.Add(device); } /// /// Add a state to the state machine. /// In this way a sequence can be created programtically. /// /// State public static void AddState(State state) { if (state != null && !states.Contains(state)) states.Add(state); } /// /// Remove the state from the state machine. /// /// State public static void RemoveState(State state) { if (state != null && state != State.CurrentState && states.Contains(state)) states.Remove(state); } /// /// Get a state from a label /// /// /// The matching state or null static State GetStateFromLabel(string label) { if (label == null) return null; foreach (State state in states) { if (state.Label != null && state.Label.Equals(label)) return state; } return null; } /// /// Start the state machine in the state 'label' in a desired mode of operation. /// This method is called in the UI thread and creates a new state machine thread. /// This method call should be embedded in: try { StateMachine.Start(...); } catch { } /// to handle configuration problems. Calls CreateDevices(mode) and CreateStates(). /// /// Mode of operation /// A copy of bench data used by the state machine /// Identifies the initial state #if DN100 public static void InitializeBoardEtc(ControlCom2VB.ControlCom2panel ctrlBrdComponent) #elif MUNICH public static void InitializeBoardEtc(ControlComponent3Munich.UserControl1 ctrlBrdComponent) #elif FUZHOU150 public static void InitializeBoardEtc(ControlComponent3Munich.UserControl1 ctrlBrdComponent) #elif FUZHOU300 public static void InitializeBoardEtc(ControlComponent3F300.UserControl1 ctrlBrdComponent) #elif PT200IL public static void InitializeBoardEtc(ControlComponent_Izrael2014.UserControl1 ctrlBrdComponent) #else /// all newer benches public static void InitializeBoardEtc(ControlComponent_Torino2015.UserControl1 ctrlBrdComponent) #endif { /// Load the list of components (entities) from the database. /// Then create the components (derived from IComponent). components = BenchControl.TbfComponents.LoadComponentsFromDB(FluentCommon.CreateSession(Database.Bench)); MasterValves = GenericDevices.ValveBase.MasterValves(components); CoupledValves = GenericDevices.ValveBase.CoupledValves(components); ExtendedValves = GenericDevices.ValveBase.ExtendedValves(components); /// Find all balances (to initialize tank capacities in the control board) /// Find the control board IList balances = new List(); SequenceBase.FlowMeters = new List(); SequenceBase.RegulValves = new List(); SequenceBase.PumpsWithFM = new List(); SequenceBase.WaterMeters = new List(); SequenceBase.Cameras = new List(); ulong valvesToInvert = 0; foreach (var cmpnt in components) { if (cmpnt is Elde.ControlBoardDev) ControlBoard = cmpnt as Elde.ControlBoardDev; if (cmpnt is IBenchInfo) SequenceBase.BenchInfo = cmpnt as IBenchInfo; if (cmpnt is IFlowMeter) SequenceBase.FlowMeters.Add(cmpnt as IFlowMeter); if ((cmpnt is IRegulValve) && !(cmpnt is BenchControl.Elde.RegulValveTandem.RegulValveTandem)) { SequenceBase.RegulValves.Add(cmpnt as IRegulValve); } if (cmpnt is IPumpFM) SequenceBase.PumpsWithFM.Add(cmpnt as IPumpFM); if (cmpnt is IWaterMeter) SequenceBase.WaterMeters.Add(cmpnt as IWaterMeter); if (cmpnt is ICamera) SequenceBase.Cameras.Add(cmpnt as ICamera); if (cmpnt is GenericDevices.IAmbient) Ambient = cmpnt as GenericDevices.IAmbient; if (cmpnt is IBalance) { IBalance balance = cmpnt as IBalance; balances.Add(balance); if (balance.BalanceNr == 0) Balance1 = balance; else if (balance.BalanceNr == 1) Balance2 = balance; else if (balance.BalanceNr == 2) Balance3 = balance; } Elde.Valve.Valve eldeValve = (cmpnt as Elde.Valve.Valve); if ((eldeValve != null) && eldeValve.Inverted) valvesToInvert |= eldeValve.Mask; cmpnt.StartChangeHandler(); /// Start handling parameter change events } /// Create an array with tank capacities float[] tankCapacities = new float[balances.Count]; for (int i = 0; i < tankCapacities.Length; i++) tankCapacities[i] = balances[i].Capacity; /// Pre-initialize the control board (= buffer the arguments ctrlBrdComponent, tankCapacities) ControlBoard.InitializeComponent(ctrlBrdComponent, valvesToInvert, tankCapacities); } public static string InitializeDevices() { bool anyComponentIsInSimulMode = false; StringBuilder inSimulMode = new StringBuilder(); /// Add all devices to the state machine and initialize them foreach (var cmpnt in components) { if (cmpnt.DebugLevel == Entities.DebugMode.Simulate) { inSimulMode.AppendFormat("{0}{1}", anyComponentIsInSimulMode ? ", " : "", cmpnt.Name); anyComponentIsInSimulMode = true; } if (cmpnt is IDevice) { AddDevice(cmpnt as IDevice); (cmpnt as IDevice).Initialize(); } } /// Propagate debug levels from parents to children when necessary foreach (var cmpnt in components) { if (cmpnt.Cfg is IChildComponentCfg && !string.IsNullOrEmpty(cmpnt.Cfg.ParentName) && (cmpnt.Cfg.DebugLevel == Entities.DebugMode.Inherit || cmpnt.Cfg.DebugLevel == Entities.DebugMode.AutoDetect)) { foreach (var par in components) { if (par.Cfg.Name.Equals(cmpnt.Cfg.ParentName)) { cmpnt.Cfg.DebugLevel = par.Cfg.DebugLevel; break; } } } } if (anyComponentIsInSimulMode) return inSimulMode.ToString(); else return null; } /// /// Start the state machine /// public static void Start() { if (stateMachineRunning) return; workerThread = new Thread(Worker); workerThread.CurrentCulture = Thread.CurrentThread.CurrentCulture; workerThread.CurrentUICulture = Thread.CurrentThread.CurrentUICulture; workerThread.Start(); stateMachineRunning = true; } public static void LoadProcedure(bool loadPathsOnly) { ISession session = FluentCommon.CreateSession(Database.Procedures); WtSession = session; feedingPaths = session.CreateQuery("FROM FeedingPath ORDER BY ItemNr").List(); benchPaths = session.CreateQuery("FROM BenchPath ORDER BY ItemNr").List(); outputPaths = session.CreateQuery("FROM OutputPath ORDER BY ItemNr").List(); metersPaths = session.CreateQuery("FROM MetersPath ORDER BY ItemNr").List(); TransitionSequences = session.CreateQuery("FROM TransitionSequence ORDER BY ItemNr").List(); // Automatic detection of empty tank valves foreach (var opath in outputPaths) { if ((EmptyTankValve1 == null) && (Balance1 != null) && (opath.Balance == Balance1.Cfg.Name)) { EmptyTankValve1 = TbfComponents.FindComponent(opath.EmptyTankValve) as IValve; } if ((EmptyTankValve2 == null) && (Balance2 != null) && (opath.Balance == Balance2.Cfg.Name)) { EmptyTankValve2 = TbfComponents.FindComponent(opath.EmptyTankValve) as IValve; } if ((EmptyTankValve3 == null) && (Balance3 != null) && (opath.Balance == Balance3.Cfg.Name)) { EmptyTankValve3 = TbfComponents.FindComponent(opath.EmptyTankValve) as IValve; } } if (loadPathsOnly) return; IList selectedProcs = session .CreateQuery("FROM Procedure WHERE ProcedureState = 'Active' AND Name = :name") .SetParameter("name", TBF.UiBridge.Bridge.SelectedProcedureName) .List(); if (selectedProcs.Count != 1) return; Procedure = selectedProcs[0]; Tests = selectedProcs[0].Tests; } public static void LoadProcedureParams(Entities.Procedure procedure) { foreach (var cmpnt in components) { cmpnt.Cfg.UpdateProcedureParams(procedure); } } public static void LoadTestParams(Entities.Test test) { foreach (var cmpnt in components) { cmpnt.Cfg.UpdateTestParams(test); } } /// /// Processes the selection done by the bench control panel in the main sequence. /// /// Selection.Q1, .Q2, .Q3 or .Test /// The selected test or null public static Entities.Test GetTest(Sequences.MainSeq.Selection selection) { if ((selection == Sequences.MainSeq.Selection.Q1) && (Tests.Count >= 1)) { return Tests[0]; } else if ((selection == Sequences.MainSeq.Selection.Q2) && (Tests.Count >= 2)) { return Tests[1]; } else if ((selection == Sequences.MainSeq.Selection.Q3) && (Tests.Count >= 3)) { return Tests[2]; } else if (selection == Sequences.MainSeq.Selection.Test) { foreach (var test in Tests) { if (test.Name.Equals(TBF.UiBridge.Bridge.SelectedTestName)) return test; } } return null; } /// /// Called from the sequence to update paths based on the selected test /// /// Selected test /// /// /// /// /// Transition sequence entity /// Transition sequence entity /// Error message in case of incorrect configuration /// true when loaded configuration is correct (all four paths are defined !=null, etc.) public static bool GetPaths(Entities.Test test, out FeedingPath pfeed, out BenchPath pben, out OutputPath pout, out MetersPath pmtrs, out Entities.TransitionSequence transitionBefore, out Entities.TransitionSequence transitionAfter, out string errorMsg) { pfeed = null; pben = null; pout = null; transitionBefore = null; transitionAfter = null; foreach (var path in feedingPaths) { if (test.FeedingPath == path.Name) { pfeed = new FeedingPath(path, components); break; } } foreach (var path in benchPaths) { if (test.BenchPath == path.Name) { pben = new BenchPath(path, components); break; } } foreach (var path in outputPaths) { if (test.OutputPath == path.Name) { pout = new OutputPath(path, components); break; } } pmtrs = GetMetersPath(test); foreach (var tr in TransitionSequences) { if (tr.Name == test.RelTransBefore) transitionBefore = tr; if (tr.Name == test.TransitionAfter) transitionAfter = tr; } if ((pfeed == null) || (pben == null) || (pout == null) || (pmtrs == null)) { errorMsg = "Cannot load paths"; return false; } if (pout.Balance == null) { errorMsg = string.Format("No balance specified in path {0}", test.OutputPath); return false; } if (Program.LocalSettings.RealDensity < 500.0f || Program.LocalSettings.RealDensity > 2000.0f) { errorMsg = string.Format("Density was not specified"); return false; } errorMsg = string.Empty; return true; } /// /// Updates paths based on the selected test /// public static MetersPath GetMetersPath(Entities.Test test) { MetersPath pmtrs = null; foreach (var path in metersPaths) { if (test.MetersPath == path.Name) { pmtrs = new MetersPath(path, components); break; } } if (pmtrs != null) { int count = Math.Min(Program.WMsCount, pmtrs.RegisterReaders.Length); for (int i = 0; i < count; i++) { if ((pmtrs.RegisterReaders[i] != null) && (pmtrs.RegisterReaders[i].Cfg.DebugLevel == Entities.DebugMode.DetectedOff)) { pmtrs.RegisterReaders[i] = null; } } } return pmtrs; } /// /// Stops the state machine (and the worker thread) /// public static void Stop() { if (stateMachineRunning) quitStateMachine = true; } /* * This is and example sequence of RunDeviceBefore() / RunOperations() / RunDeviceAfter() calls * as they are executed during normal run from the progran start to the end. * foreach (var device in devices) device.RunDeviceBefore(); . . . . . . in StateMachine.Worker() State.Create(...).AddOperation(...).AddOperation(...).EnterState() . . in the sequence in Execute(...) foreach (var device in devices) device.RunDeviceAfter(); . . . . . . . in WaitRunDevsRunOps() WaitNextTick() (may throw QuitStateMachineException) . . . . . . . . in WaitRunDevsRunOps() foreach (var device in devices) device.RunDeviceBefore(); . . . . . . in WaitRunDevsRunOps() IList events = State.RunOperations(); . . . . . . . . . . . . . in WaitRunDevsRunOps() foreach (var device in devices) device.RunDeviceAfter(); . . . . . . . in WaitRunDevsRunOps() WaitNextTick() (may throw QuitStateMachineException) . . . . . . . . in WaitRunDevsRunOps() foreach (var device in devices) device.RunDeviceBefore(); . . . . . . in WaitRunDevsRunOps() IList events = State.RunOperations(); . . . . . . . . . . . . . in WaitRunDevsRunOps() State.Create(...).AddOperation(...).AddOperation(...).EnterState() . . in the sequence in Execute(...) foreach (var device in devices) device.RunDeviceAfter(); . . . . . . . in WaitRunDevsRunOps() WaitNextTick() (may throw QuitStateMachineException) . . . . . . . . in WaitRunDevsRunOps() foreach (var device in devices) device.RunDeviceBefore(); . . . . . . in WaitRunDevsRunOps() IList events = State.RunOperations(); . . . . . . . . . . . . . in WaitRunDevsRunOps() foreach (var device in devices) device.RunDeviceAfter(); . . . . . . . in WaitRunDevsRunOps() WaitNextTick() (assume QuitStateMachineException thrown) . . . . . . in WaitRunDevsRunOps() State.StopOperations(); . . . . . . . . . . . . . . . . . . . . . . . in StateMachine.Worker() catch() foreach (var device in devices) device.StopDevice(); . . . . . . . . . in StateMachine.Worker() catch() */ /// /// Worker thread: calls Start(), Run() and Stop() methods of operations. /// It uses 'currentState', 'nextState' and 'quitStateMachine' static fields. /// static void Worker() { startDateTime = DateTime.Now; wlog.InfoFormat(" currentTime = {0}s startDateTime = {1}", currentTimeSec.ToString(), startDateTime.ToString()); /// Run all devices for the first time foreach (var device in devices) device.RunDeviceBefore(); try { SequenceBase.ReferenceFlowmetersCount = SequenceBase.FlowMeters.Count; SequenceBase.LtrPerRefPulse = new float[SequenceBase.ReferenceFlowmetersCount]; foreach (var flowmtr in SequenceBase.FlowMeters) { int ix = flowmtr.Idx1; if (ix > 0 && ix <= SequenceBase.ReferenceFlowmetersCount) { SequenceBase.LtrPerRefPulse[ix - 1] = flowmtr.NominalFlow / 7200.0f; } } (new Sequences.MainSeq()).Execute(null); } catch (QuitStateMachineException) { State.StopOperations(); foreach (var device in devices) device.StopDevice(); quitStateMachine = false; stateMachineRunning = false; } } /// /// Do stuff that is repeated in the state execution loops most often /// /// List of Event-s returned from the state operations public static IList WaitRunDevsRunOps() { foreach (var device in devices) device.RunDeviceAfter(); WaitNextTick(); foreach (var device in devices) device.RunDeviceBefore(); IList events = State.RunOperations(); return events; /// This is followed by a state change in the sequence } /// /// Wait time period - synchronize /// /// true when interrupted by 'quitStateMachine', otherwise false public static void WaitNextTick() { currentTimeSec += Period; TimeSpan timeFromStart = TimeSpan.FromSeconds(currentTimeSec); DateTime nextLoopDateTime = startDateTime + timeFromStart; while (DateTime.Now < nextLoopDateTime) { if (quitStateMachine) { quitStateMachine = false; wlog.Info("quitStateMachine == true ... going to stop the StateMachine()"); throw new QuitStateMachineException(); } Thread.Sleep(100); } wlog.DebugFormat(" currentTime = {0}s", currentTimeSec); } } }