tbf/TestBenchFramework/BenchControl/StateMachine.cs

636 lines
24 KiB
C#

///
/// 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 Config.Entities;
using TBF.BenchControl.Generic;
using TBF.BenchControl.GenericDevices;
using TBF.BenchControl.Sequences;
namespace TBF.BenchControl
{
public class QuitStateMachineException : Exception
{
}
/// <summary>
/// This class controls real test bench behavior.
/// It is based on a state machine
/// </summary>
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<IComponent> components; /// list of all components
static IList<IDevice> devices; /// list of devices
///
/// Bench paths
static IList<Config.Entities.FeedingPath> feedingPaths;
static IList<Config.Entities.BenchPath> benchPaths;
static IList<Config.Entities.OutputPath> outputPaths;
static IList<Config.Entities.MetersPath> metersPaths;
public static IList<Config.Entities.TransitionSequence> 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<IValve> MasterValves; /// list of directly controlled valves
public static IList<IValve> CoupledValves; /// list of coupled valves
public static IList<Elde.ValveEx.Valve> 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<State> states; /// list of states
static DateTime cycleStartTimeStamp;
public static DateTime CycleStartTimeStamp
{
get { return cycleStartTimeStamp; }
set
{
cycleStartTimeStamp = value;
CycleStartYY = (cycleStartTimeStamp.Year % 100).ToString("D2");
CycleStartMM = cycleStartTimeStamp.Month.ToString("D2");
CycleStartDD = cycleStartTimeStamp.Day.ToString("D2");
CycleStartHH_MI_SS = string.Format("{0}_{1}_{2}",
cycleStartTimeStamp.Hour.ToString("D2"),
cycleStartTimeStamp.Minute.ToString("D2"),
cycleStartTimeStamp.Second.ToString("D2"));
}
}
public static string CycleStartYY;
public static string CycleStartMM;
public static string CycleStartDD;
public static string CycleStartHH_MI_SS;
public static IList<IValve> 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<IValve> 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)
);
}
}
/// <summary>
/// Loaded by LoadProcedure() or IOperation LoadProcedureOp(...)
/// </summary>
public static Procedure Procedure; /// Procedure
public static IList<Test> Tests; /// Tests
/// Hardware devices connected to the PC controlling the bench.
public static IList<IComponent> Components { get { return components; } }
public static IList<IDevice> Devices { get { return devices; } }
/// <summary>
/// DateTime of time instance when the state machine worker thread starts
/// </summary>
public static DateTime StartDateTime { get { return startDateTime; } }
/// <summary>
/// Current state name
/// </summary>
public static int Time { get { return currentTimeSec; } }
/// <summary>
/// Constructor
/// </summary>
static StateMachine()
{
devices = new List<IDevice>();
states = new List<State>();
currentTimeSec = 0;
quitStateMachine = false;
}
/// <summary>
/// Add a device to the state machine.
/// </summary>
/// <param name="obj">Device</param>
public static void AddDevice(IDevice device)
{
if (device != null && !devices.Contains(device)) devices.Add(device);
}
/// <summary>
/// Add a state to the state machine.
/// In this way a sequence can be created programtically.
/// </summary>
/// <param name="obj">State</param>
public static void AddState(State state)
{
if (state != null && !states.Contains(state)) states.Add(state);
}
/// <summary>
/// Remove the state from the state machine.
/// </summary>
/// <param name="obj">State</param>
public static void RemoveState(State state)
{
if (state != null && state != State.CurrentState && states.Contains(state)) states.Remove(state);
}
/// <summary>
/// Get a state from a label
/// </summary>
/// <param name="label"></param>
/// <returns>The matching state or null</returns>
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;
}
/// <summary>
/// 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().
/// </summary>
/// <param name="mode">Mode of operation</param>
/// <param name="benchData">A copy of bench data used by the state machine</param>
/// <param name="label">Identifies the initial state</param>
#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(Config.FluentCommon.CreateSession(Config.Entities.DBKind.Config));
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<IBalance> balances = new List<IBalance>();
SequenceBase.FlowMeters = new List<IFlowMeter>();
SequenceBase.RegulValves = new List<IRegulValve>();
SequenceBase.PumpsWithFM = new List<IPumpFM>();
SequenceBase.WaterMeters = new List<IWaterMeter>();
SequenceBase.Cameras = new List<ICamera>();
ulong valvesToInvert = 0;
foreach (var cmpnt in components)
{
if (cmpnt is Elde.ControlBoardDev) ControlBoard = cmpnt as Elde.ControlBoardDev;
if (cmpnt is IBenchInfo) ProcessData.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 == 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 == DebugMode.Inherit || cmpnt.Cfg.DebugLevel == 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;
}
/// <summary>
/// Start the state machine
/// </summary>
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 = Config.FluentCommon.CreateSession(Config.Entities.DBKind.Config);
WtSession = session;
feedingPaths = session.CreateQuery("FROM FeedingPath ORDER BY ItemNr").List<Config.Entities.FeedingPath>();
benchPaths = session.CreateQuery("FROM BenchPath ORDER BY ItemNr").List<Config.Entities.BenchPath>();
outputPaths = session.CreateQuery("FROM OutputPath ORDER BY ItemNr").List<Config.Entities.OutputPath>();
metersPaths = session.CreateQuery("FROM MetersPath ORDER BY ItemNr").List<Config.Entities.MetersPath>();
TransitionSequences = session.CreateQuery("FROM TransitionSequence ORDER BY ItemNr").List<TransitionSequence>();
// 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<Procedure> selectedProcs = session
.CreateQuery("FROM Procedure WHERE ProcedureState = 'Active' AND Name = :name")
.SetParameter("name", TBF.UiBridge.Bridge.SelectedProcedureName)
.List<Procedure>();
if (selectedProcs.Count != 1) return;
Procedure = selectedProcs[0];
Tests = selectedProcs[0].Tests;
}
public static void LoadProcedureParams(Procedure procedure)
{
foreach (var cmpnt in components)
{
cmpnt.Cfg.UpdateProcedureParams(procedure);
}
}
public static void LoadTestParams(Test test)
{
foreach (var cmpnt in components)
{
cmpnt.Cfg.UpdateTestParams(test);
}
}
/// <summary>
/// Processes the selection done by the bench control panel in the main sequence.
/// </summary>
/// <param name="selection">Selection.Q1, .Q2, .Q3 or .Test</param>
/// <returns>The selected test or null</returns>
public static Config.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 (Utils.TestTitle(test, 1).Equals(UiBridge.Bridge.SelectedTestName)) return test;
}
}
return null;
}
/// <summary>
/// Called from the sequence to update paths based on the selected test
/// </summary>
/// <param name="test">Selected test</param>
/// <param name="pfeed"></param>
/// <param name="pben"></param>
/// <param name="pout"></param>
/// <param name="pmtrs"></param>
/// <param name="transitionBefore">Transition sequence entity</param>
/// <param name="transitionAfter">Transition sequence entity</param>
/// <param name="errorMsg">Error message in case of incorrect configuration</param>
/// <returns>true when loaded configuration is correct (all four paths are defined !=null, etc.)</returns>
public static bool GetPaths(Test test,
out FeedingPath pfeed,
out BenchPath pben,
out OutputPath pout,
out MetersPath pmtrs,
out TransitionSequence transitionBefore,
out 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;
}
/// <summary>
/// Updates paths based on the selected test
/// </summary>
public static MetersPath GetMetersPath(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(Config.Data.WMsCount, pmtrs.RegisterReaders.Length);
for (int i = 0; i < count; i++)
{
if ((pmtrs.RegisterReaders[i] != null) &&
(pmtrs.RegisterReaders[i].Cfg.DebugLevel == DebugMode.DetectedOff))
{
pmtrs.RegisterReaders[i] = null;
}
}
}
return pmtrs;
}
/// <summary>
/// Stops the state machine (and the worker thread)
/// </summary>
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<Event> 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<Event> 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<Event> 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()
*/
/// <summary>
/// Worker thread: calls Start(), Run() and Stop() methods of operations.
/// It uses 'currentState', 'nextState' and 'quitStateMachine' static fields.
/// </summary>
static void Worker()
{
startDateTime = DateTime.Now;
CycleStartTimeStamp = startDateTime; /// To prevent it is undefined
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;
}
}
/// <summary>
/// Do stuff that is repeated in the state execution loops most often
/// </summary>
/// <returns>List of Event-s returned from the state operations</returns>
public static IList<Event> WaitRunDevsRunOps()
{
foreach (var device in devices) device.RunDeviceAfter();
WaitNextTick();
foreach (var device in devices) device.RunDeviceBefore();
IList<Event> events = State.RunOperations();
return events;
/// This is followed by a state change in the sequence
}
/// <summary>
/// Wait time period - synchronize
/// </summary>
/// <returns>true when interrupted by 'quitStateMachine', otherwise false</returns>
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);
}
}
}