/// /// Copyright (c) 2021 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.Diagnostics; using System.IO.Ports; using System.Linq; using System.Text; using log4net; using Common; using Config.Entities; using Dirichlet.Numerics; using TBF.Rig.GenericDevices; using TBF.Rig.Sequences; using TBF.UiBridge; namespace TBF.Rig.ControlBoard.Uni { public class UniCB : ComponentBase, IControlBoard, SchematicDrawing.IDrawingItCmpntWithMeasuredVal { private static readonly ILog log = LogManager.GetLogger(typeof(UniCB)); public override string ToString() { return string.Format("CBoard({0})", cbCfg != null ? cbCfg.ToString(-1) : string.Empty); } /// TBF component configuration and useful wrappers readonly UniCBCfg cbCfg; string comPort { get { return string.Format("COM{0}", cbCfg.ComPortNr); } } short config { get { return (short)((cbCfg.DrainValvesCount << 8) + ((cbCfg.DivertersCount - 1) << 3) + cbCfg.RegValvesCount - 4); } } /// IDrawingItCmpnt interface implementation public SchematicDrawing.IDrawingItem DrawingItem { get { return cbCfg as SchematicDrawing.IDrawingItem; } } public bool MsrmntAvailable { get { return true; } } public double MeasuredVal { get { return 0; } } StringBuilder altString = new StringBuilder(); public string AltString { get { altString.Clear(); altString.AppendFormat("State: {0}.{1}.{2}.{3}\n", (State >> 48) & 0xFF, (State >> 40) & 0xFF, (State >> 32) & 0xFF, Convert.ToUInt32(State & 0xFFFFFFFF).ToString("X6")); altString.AppendFormat("DI: {0}\n", ToBin40(DigitalInputs)); altString.AppendFormat("DO: {0}\n", ToBin40(((UInt64)Route) >> 8)); altString.AppendFormat("AI0: {0:D3} AI1: {1:D3} AI2: {2:D3} AI3: {3:D3} AI4: {4:D3} AI5: {5:D3}\n", AnalogInput(0), AnalogInput(1), AnalogInput(2), AnalogInput(3), AnalogInput(4), AnalogInput(5)); altString.AppendFormat("AI8: {0:D3} AI9: {1:D3} AI10: {2:D3} AI11: {3:D3}\n", AnalogInput(8), AnalogInput(9), AnalogInput(10), AnalogInput(11)); altString.AppendFormat("Frequency: {0:F1} Hz Pulses: {1}", RefFrequency, RefPulses); return altString.ToString(); } } string ToBin40(UInt64 val) { return string.Format("{0}-{1}-{2}-{3}-{4}", ToBin8((val >> 32) & 0xFF), ToBin8((val >> 24) & 0xFF), ToBin8((val >> 16) & 0xFF), ToBin8((val >> 8) & 0xFF), ToBin8(val & 0xFF)); } string ToBin8(UInt64 val) { string str = Convert.ToString((byte)val, 2); switch (str.Length) { case 1: return "0000000" + str; case 2: return "000000" + str; case 3: return "00000" + str; case 4: return "0000" + str; case 5: return "000" + str; case 6: return "00" + str; case 7: return "0" + str; default: return str; } } /// /// Serial communication /// SerialPort serialPort; /// Seral port or null: Check serialPort.IsOpen before using the serial port const int BUFFER_SIZE = 4000; /// Size of input buffers byte[] buffer; /// Buffer for data received in one attempt in RunDeviceBefore() byte[] rcvdData; /// Buffer for all recevied but nut processed data. May contain data from several read attempts int[] cumulativeSums; int rcvdBytesCount; /// Count of valid bytes in rcvdData buffer /// /// Digital outputs, states of electromagnetic valves /// UInt128 benchModelRoute; UInt128 valvesToInvert; /// Propagated from configurations of other components readonly UInt128 routeMask; /// This mask masks out virtual valves, routeMask is set in the constructor bool outputsInitialized; int lastStateMachineTime = 0; /// UI actions are blocked when true public bool IsUIBlocked { get; set; } /// Message queue Queue actionQueue; /// Received data public GeneralData Data; DivTransitionData divTransitionData; ScopeAnalyzerData scopeAnalyzerData; SwitchCountersData switchCounterData; public UInt64 State { get { return Data.State; } } /// Control board state /// Current flow meter ID public int FlowmeterId { get { return Convert.ToInt32(Data.State & (ulong)StatusP.FlowmtrNrMask); } } public UInt64 DigitalInputs { get { return Data.Vstupy; } } /// Digital inputs public Int16 AnalogInput(int channel) { if (Data.AnalogInput != null && channel >= 0 && channel < Data.AnalogInput.Length) { return Data.AnalogInput[channel]; } return 0; } /// /// Route: 128-bit word representing the current state of all valves and pumps /// public UInt128 Route { get { return benchModelRoute; } } public UInt128 RRoute { get { return (UInt128)Data.Vystupy; } } /// Values valid during a test public double RefFrequency { get { return Data.ReferenceFreq[0]; } } public double RefFrequency1 { get { return Data.ReferenceFreq[1]; } } public double RefFrequency2 { get { return Data.ReferenceFreq[2]; } } public double RefFrequency3 { get { return Data.ReferenceFreq[3]; } } /// Last test results public double TestTime { get { return Data.Ttime; } } public int RefPulses { get { return Data.EtPulses[0]; } } public float ValveOpenCloseTime { set; get; } /// Start valve open or close time in sec /// /// Select devices used for control and measurement /// /// public OutputPath Devices { set { devices = value; } get { return devices; } } OutputPath devices; IFlowMeter flowMeter { get { return devices.FlowMeter; } } IRegValve regValve { get { return devices.RegValve; } } IDiverter diverter { get { return devices.Diverter; } } /// /// Constructor for user interfaces (change settings only). /// Initialize(), RunDeviceXY() and StopDevice() methods of the created object will not be called. /// public UniCB() { devices = new OutputPath(string.Empty, 0); } /// /// Constructor invoked when starting the test bench and creating components. /// public UniCB(Generic.IComponentCfg cfg) : base(cfg) { cbCfg = cfg as UniCBCfg; devices = new OutputPath(string.Empty, 0); actionQueue = new Queue(); actionQueue.Enqueue(Action.Stop(false)); /// Stop all outputsInitialized = false; Data = new GeneralData(); divTransitionData = null; scopeAnalyzerData = null; switchCounterData = null; serialPort = null; buffer = new byte[BUFFER_SIZE]; rcvdData = new byte[BUFFER_SIZE]; cumulativeSums = new int[BUFFER_SIZE]; rcvdBytesCount = 0; routeMask = 0; for (int i = 0; i < 128; i++) { if (i < cbCfg.VirtValvesRangeLo || i > cbCfg.VirtValvesRangeHi) { routeMask = routeMask | ((UInt128)1 << i); } } TBF.UiBridge.Bridge.RouteChangeHandler += delegate(object sndr, TBF.UiBridge.RouteChangeArgs args) { UInt128 mask = ((UInt128)1) << args.BitNr; if ((benchModelRoute & mask) == 0) { benchModelRoute |= mask; } else { benchModelRoute &= (~mask); } }; log.Warn(this.ToString()); } /// /// Specify inverted valves /// /// public void SpecifyInvertedValves(UInt128 valvesToInvert) { this.valvesToInvert = valvesToInvert; this.benchModelRoute = valvesToInvert; } #region IDevice interface /// /// Initialize control board device hardware /// public void Initialize() { if (cbCfg.DebugLevel != DebugMode.Normal) { log.FatalFormat("{0} - Device simulated", Name); return; } serialPort = new SerialPort(comPort, 9600, Parity.None, 8, StopBits.One); serialPort.DtrEnable = true; serialPort.WriteTimeout = 4000; serialPort.ParityReplace = 0xFF; serialPort.Open(); log.FatalFormat("{0} - Device successfully initialized", Name); } /// /// Reecive and process data from the control board serial port /// public void RunDeviceBefore() { if (cbCfg.DebugLevel != DebugMode.Normal || serialPort == null || !serialPort.IsOpen) return; /// Read new received bytes and append them to a buffer int bytesCount = serialPort.BytesToRead; if (bytesCount > 0) { bytesCount = serialPort.Read(buffer, 0, Math.Min(bytesCount, BUFFER_SIZE)); } for (int i = 0; i < bytesCount && rcvdBytesCount < BUFFER_SIZE; i++, rcvdBytesCount++) { rcvdData[rcvdBytesCount] = buffer[i]; } /// Calculate cumulative sums (to verify checksums easily) int cumulativeSum = 0; for (int i = 0; i < rcvdBytesCount; i++) { cumulativeSum += rcvdData[i]; cumulativeSums[i] = cumulativeSum; } /// Try to detect various kinds of data in rcvdData buffer in a loop, start at offset 0 int offset = 0; int lastValidOffset = 0; /// Offset of the first byte after the last successfully parsed frame do { bool anyProcessed = false; if (ScopeAnalyzerData.FrameFitsData(rcvdData, cumulativeSums, rcvdBytesCount, offset)) { /// Scope analyzer data received scopeAnalyzerData = ScopeAnalyzerData.GetData(rcvdData, offset); string s = Telegram.LogTelegram("Scope analyzer data ", rcvdData, offset, ScopeAnalyzerData.MessageLen); Debug.WriteLine(s); log.Debug(s); offset += ScopeAnalyzerData.MessageLen; lastValidOffset = offset; anyProcessed = true; } else if (GeneralData.FrameFitsData(rcvdData, cumulativeSums, rcvdBytesCount, offset)) { /// Regular data received Data.UpdateData(rcvdData, offset); string s = Telegram.LogTelegram("General data ", rcvdData, offset, GeneralData.MessageLen); Debug.WriteLine(s); log.Debug(s); offset += GeneralData.MessageLen; lastValidOffset = offset; anyProcessed = true; } else if (SwitchCountersData.FrameFitsData(rcvdData, cumulativeSums, rcvdBytesCount, offset)) { /// Switch counter data received switchCounterData = SwitchCountersData.GetData(rcvdData, offset); string s = Telegram.LogTelegram("Switch counter data ", rcvdData, offset, SwitchCountersData.MessageLen); Debug.WriteLine(s); log.Debug(s); offset += SwitchCountersData.MessageLen; lastValidOffset = offset; anyProcessed = true; } else if (DivTransitionData.FrameFitsData(rcvdData, cumulativeSums, rcvdBytesCount, offset)) { /// Diverter transition data received divTransitionData = DivTransitionData.GetData(rcvdData, offset); string s = Telegram.LogTelegram("Diverter transition data ", rcvdData, offset, DivTransitionData.MessageLen); Debug.WriteLine(s); log.Debug(s); offset += DivTransitionData.MessageLen; lastValidOffset = offset; anyProcessed = true; } if (!anyProcessed) offset++; } while (offset < rcvdBytesCount); /// Remove processed data from the receive buffer for (int i = lastValidOffset; i < rcvdBytesCount; i++) { rcvdData[i - lastValidOffset] = rcvdData[i]; } rcvdBytesCount -= lastValidOffset; /// Synchronize benchModelRoute with RRoute SyncRoute(); } /// /// Set Route to value returned by RRoute. Take virtual valves into account. /// /// 'ulong' with bits of valves that have changed set to '1' public UInt128 SyncRoute() { UInt128 oldRoute = benchModelRoute; benchModelRoute = (RRoute & routeMask) | (benchModelRoute & (~routeMask)); log.DebugFormat("SyncRoute(), new route = {0}", Utils.RouteToStr(benchModelRoute)); return oldRoute ^ benchModelRoute; } /// /// Send data to the control board serial port /// public void RunDeviceAfter() { if (cbCfg.DebugLevel != DebugMode.Normal) return; /// Make sure the serial port is open if (serialPort == null || !serialPort.IsOpen) { /// Serial port is closed or does not exist => re-open the serial port try { if (serialPort == null) { serialPort = new SerialPort(comPort, 9600, Parity.None, 8, StopBits.One); } serialPort.DtrEnable = true; serialPort.WriteTimeout = 4000; serialPort.ParityReplace = 0xFF; serialPort.Open(); } catch (Exception) { /// Re-opening serial port failed return; } } if (!outputsInitialized || RRoute != benchModelRoute) /// || (StateMachine.Time - lastStateMachineTime) >= 15) { var actionsToModify = actionQueue.Where(x => (x.ActionId == ActionID.ChangeRoute)); bool oneModified = false; foreach (var a in actionsToModify) { if (oneModified) { log.ErrorFormat("ChangeRoute !!! TWO actions of the same type in the queue"); break; /// Do not modify more actions, this should never happen } oneModified = true; a.Route = (ulong)benchModelRoute; log.InfoFormat("ChangeRoute(route={0:X}) ... an action in the queue modified", benchModelRoute); } if (!oneModified) { actionQueue.Enqueue(Action.ChangeRoute(false, (ulong)benchModelRoute)); log.InfoFormat("ChangeRoute(route={0:X}) ... a new action enqueued", benchModelRoute); } foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } var combinedAction = Action.FetchNonConflictingActions(actionQueue); /// Default action is RequestDataOnly byte[] outData = OutputMessage.GetMessage(combinedAction, config); if (outData != null && outData.Length > 0) { if ((combinedAction.ActionId & ActionID.ChangeRoute) != 0) { outputsInitialized = true; lastStateMachineTime = StateMachine.Time; } serialPort.Write(outData, 0, outData.Length); string s = Telegram.LogTelegram("Sent: ", outData); Debug.WriteLine(s); log.Debug(s); } ProcessData.UpdateMeasuredValuesAndSetpoints(); Bridge.OnStateMachineTick(this, new StateMachineTickEventArgs(Route, ProcessData.MsrmntAvailableFlags, ProcessData.MeasuredValues, ProcessData.AltStrings, ProcessData.Setpoints)); } public void StopDevice() { if (serialPort != null) serialPort.Close(); serialPort = null; } public void StopDevice2() { } #endregion #region IControlBoard interface /// /// Interface functions /// public void ClearProcessValues() { } public double TestTimeWM(int wmNr1) { if (wmNr1 >= 1 && wmNr1 < Data.ImpulseTime.Length) { return Data.ImpulseTime[wmNr1]; } return 0; } public int RefPulsesWM(int wmNr1) { if (wmNr1 >= 1 && wmNr1 < Data.EtPulses.Length) { return Data.EtPulses[wmNr1]; } return 0; } public int PulsesWM(int wmNr1) { if (wmNr1 >= 1 && wmNr1 < Data.WMeterPuls.Length) { return Data.WMeterPuls[wmNr1]; } return 0; } public UInt128 SetValves(UInt128 valvesToOpen, UInt128 valvesToClose, IList extendedValves) { UInt128 oldRoute = benchModelRoute; benchModelRoute = (((benchModelRoute ^ valvesToInvert) | valvesToOpen) & (~valvesToClose)) ^ valvesToInvert; foreach (var extV in extendedValves) extV.UpdateRoute(ref benchModelRoute); log.DebugFormat("SetValves(x,x), new route = {0}", Utils.RouteToStr(benchModelRoute)); UInt128 retVal = oldRoute ^ benchModelRoute; log.DebugFormat("{0}.SetValves({1}, {2}) returns {3}", Name, valvesToOpen.ToString("X"), valvesToClose.ToString("X"), retVal.ToString("X")); return retVal; } /// /// Operations /// public IOperation SetFlowOp(double flowLimLo, double flowLimHi, TBF.Boxes.DoubleBox measuredFlow, int timeout) { return Devices.RegValve.SetFlowOp(Devices.FlowMeter, flowLimLo, flowLimHi, measuredFlow, timeout); } public IOperation StopFlowRegulationOp() { Stop(false); //scheduledActionQueue.Enqueue(Action.Stop()); return null; } /// /// Start a standing start-stop test /// /// Operation running a test public IOperation StandingStartStopTestOp(Test test, double qFrom, double qTo, int pulsesCount) { return new StandingStartStopTestOp(this, devices, qFrom, qTo, pulsesCount); } /// /// Start a flying start-stop test /// /// Operation running a test public IOperation FlyingStartStopTestOp(Test test, double qFrom, double qTo, int pulsesCount) { return new FlyingStartStopTestOp(this, devices, qFrom, qTo, pulsesCount); } /// /// Start a flying start-stop test with diverter /// /// Operation running a test public IOperation FlyingStartStopTestWithDivOp(Test test, double qFrom, double qTo, int pulsesCount, int massPulsesCount = 0, bool readDivTransition = true) { return new FlyingStartStopTestWithDivOp(this, devices, qFrom, qTo, pulsesCount, massPulsesCount, readDivTransition); } public IOperation ReadDiverterTransitionOp(bool isStart, Sequences.Statistics plotter, int batchNr, string testName, int repetition) { return null; } /// /// Returns operation with events: Event.MeasurementCompleted or Event.None /// /// Operation public IOperation QueryMeasurementEndOp() { return null; } public IOperation StopPreviousOp() { return null; } #endregion #region IValveControl interface /// /// Open/close one valve. /// Events: ValvesSet, ValvesBusy, Error /// /// Valve to be opened /// Valve to be closed /// Newly created operation instance reference casted to IOperaton public IOperation SetValvesOp(IValve valveOpen, IValve valveClose) { return new BuiltIn.SetValvesOp(this, valveOpen, valveClose); } /// /// Open/close multiple valves. /// Events: ValvesSet, ValvesBusy, Error /// /// A list of valves to be opened /// A list of valves to be closed /// Newly created operation instance reference casted to IOperaton public IOperation SetValvesOp(IList valvesOpen, IList valvesClose) { return new BuiltIn.SetValvesOp(this, valvesOpen, valvesClose); } /// /// Open start/stop valve. /// Events: ValvesSet, ValvesBusy, Error /// /// Created operation public IOperation OpenStartValveOp() { return new BuiltIn.SetValvesOp(this, true, devices); } /// /// Close start/stop valve. /// Events: ValvesSet, ValvesBusy, Error /// /// Created operation public IOperation CloseStartValveOp() { return new BuiltIn.SetValvesOp(this, false, devices); } #endregion public void MeasureFlow(bool isFromUI, int flowMeterId) { if (IsUIBlocked && isFromUI) return; actionQueue.Enqueue(Action.MeasureFlow(isFromUI, flowMeterId)); log.InfoFormat("Enqueue( MeasureFlow(fm={0}) )", flowMeterId); foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } /// /// To be used with a regulation valve controlled by incremental pulses. /// Stability time is fixed: 200 ms /// public void SetFlow(bool isFromUI, int regVId, double freqLo, double freqHi) { if (IsUIBlocked && isFromUI) return; actionQueue.Enqueue(Action.SetFlow(isFromUI, regVId, freqLo, freqHi, Convert.ToInt32(Math.Round(Devices.PidCoef)), 200)); log.InfoFormat("Enqueue( SetFlow(rv={0}, fLo={1}, fHi={2}, pid={3}) )", regVId, freqLo, freqHi, Convert.ToInt32(Math.Round(Devices.PidCoef))); foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } public void StartTest(bool isFromUI, int flowMId, int divId, int divThreshold, bool isSyncMethod, bool isDivUsed, bool isStartStop, bool isProlonged, int totalPulsesCount, int massPulsesCount = -1) { if (IsUIBlocked && isFromUI) return; actionQueue.Enqueue(Action.StartTest(isFromUI, flowMId, divId, divThreshold, isSyncMethod, isDivUsed, isStartStop, isProlonged, totalPulsesCount, massPulsesCount)); log.InfoFormat("Enqueue( StartTest(fm={0}, div={1}, Sync={2}, withDiv={3}, s/s={4}, prolonged={5} pulsesCount={6} massPulsesCount={7}) )", flowMId, divId, isSyncMethod, isDivUsed, isStartStop, isProlonged, totalPulsesCount, massPulsesCount); foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } public void Stop(bool isFromUI) { if (IsUIBlocked && isFromUI) return; actionQueue.Enqueue(Action.Stop(isFromUI)); log.InfoFormat("Enqueue( Stop() )"); foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } /// /// Perform incremental movement of a regulation valve /// /// Reg. valve ID /// Time in seconds, positive value opens the reg. valve public void RegVlvIncrMove(bool isFromUI, int regVId, double time) { if (IsUIBlocked && isFromUI) return; var newAction = Action.RegVlvIncrMove(isFromUI, regVId, time); var actionsToModify = actionQueue.Where(x => (x.ActionId == ActionID.RegVlvIncrMove && x.RegVId == newAction.RegVId)); bool oneModified = false; foreach (var a in actionsToModify) { if (oneModified) { log.ErrorFormat("RegVlvIncrMove !!! TWO actions of the same type in the queue"); break; /// Do not modify more actions, this should never happen } a.RVMovePar1 += newAction.RVMovePar1; a.RVMovePar2 += newAction.RVMovePar2; oneModified = true; log.InfoFormat("RegVlvIncrMove(UI={0}, RV={1}, time={2}) ... an action in the queue modified", isFromUI, regVId, time); } if (!oneModified) { actionQueue.Enqueue(newAction); log.InfoFormat("RegVlvIncrMove(UI={0}, RV={1}, time={2}) ... a new action enqueued", isFromUI, regVId, time); } foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } /// /// Move a regulation valve to specfied position /// /// Reg. valve ID /// Reg. valve position (0 .. 1.0) public void RegVlvMoveToPos(bool isFromUI, int regVId, double positionLo, double positionHi = -1) { if (IsUIBlocked && isFromUI) return; actionQueue.Enqueue(Action.RegVlvMoveToPos(isFromUI, regVId, positionLo, positionHi)); log.InfoFormat("Enqueue( RegVlvMoveToPos(rv={0}, positionLo={1}, positionHi={2}) )", regVId, positionLo, positionHi); foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } public void SwitchDiverter(bool isFromUI, int divNr1, bool toTank) { if (IsUIBlocked && isFromUI) return; actionQueue.Enqueue(Action.SwitchDiverter(isFromUI, divNr1, toTank)); log.InfoFormat("Enqueue( SwitchDiverter(div={0}, toTank={1}) )", divNr1, toTank); foreach (var a in actionQueue) log.DebugFormat(" {0}", a); } public void ReadDiverterTransition(bool isFromUI, int divNr1) { if (IsUIBlocked && isFromUI) return; /// TODO } public void SetFiltersPidShortPulses(int[] filters, float pidCoef, int shortPulses) { /// TODO } } }