/// /// Copyright (c) 2018 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using Common; using log4net; using TBF.Rig.Generic; using TBF.Rig.GenericDevices; using TBF.Boxes; using TBF.Rig.Hart.Common; using TBF.Resources; namespace TBF.Rig.Hart.Nivotrack { /// /// Root component for Modbus communication via serial port (RS485) /// public class Nivotrack : ComponentBase, IDevice, ILevelMeter, IOperation, GenericDevices.IHasCalendarEvents { const int StableReadingsCount = 9; private static readonly ILog log = LogManager.GetLogger(typeof(Nivotrack)); public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } readonly NivotrackCfg nivotrackCfg; readonly IHart hart; public double Level { get { return ReadLevel(); } } double rawLevel_mm; bool isRawLevelValid; /// For stable measurement calculation double maxSDev; double[] levelReadings; double[] sortedLevelReadings; int currentReadingsCount; bool measurementCompleted; /// stableMeasurementCompleted DoubleBox levelBox; /// enum CurrentOp { None, ReadLevel, ReadStableLevel, } CurrentOp currentOp; public Nivotrack() { } /// /// Ambient temperature / humidity / pressure meter 'Greco' connected via serial interface (RS232) /// Connection settings: 19200 Bd 8-bits No-parity 1-stop-bit Flow control: none or hardware. /// public Nivotrack(Generic.IComponentCfg cfg, IList components) : base(cfg) { nivotrackCfg = cfg as NivotrackCfg; hart = (IHart)TbfComponents.FindComponent(cfg.ParentName, components); if (hart == null) throw new Exception("Cannot find " + Name + " parent"); } public override void Initialize() { levelReadings = new double[StableReadingsCount]; sortedLevelReadings = new double[StableReadingsCount]; rawLevel_mm = 0; isRawLevelValid = false; log.FatalFormat("{0} initialized: {1}", Name, this); } /// /// Calendar support /// public IList GetCalendarEvents() { DateTime calibrationDue = nivotrackCfg.CalibValidDate; IList calendarEvents = new List(); if (calibrationDue > TBF.UI.Constants.MinDate) { /// Calibration due date calendar event calendarEvents.Add(new TBF.UI.Calendar.CalibrationDueDateEvent(calibrationDue.Date, Name, string.Format(Strings.Calibration_due_date_is_0, calibrationDue.Date.ToString(TBF.UI.Constants.DateFormat)))); if (DateTime.Now.Date <= calibrationDue.AddDays(-7)) { /// Weekly reminders (last 5 weeks) calendarEvents.Add(new TBF.UI.Calendar.CalibrationReminderEvent(calibrationDue.Date.AddDays(-35), Name, string.Format(Strings.Calibration_due_date_is_0, calibrationDue.Date.ToString(TBF.UI.Constants.DateFormat)), false)); } if (DateTime.Now.Date <= calibrationDue.Date) { /// Daily reminders (last 5 days) calendarEvents.Add(new TBF.UI.Calendar.CalibrationReminderEvent(calibrationDue.AddDays(-5), Name, string.Format(Strings.Calibration_due_date_is_0, calibrationDue.Date.ToString(TBF.UI.Constants.DateFormat)), true)); } } return calendarEvents; } void Detect() { byte bcnt = (byte)0; /// block count byte[] data = new byte[5]; data[0] = (byte)Mark.ShortFrameMaster2Slave; data[1] = (byte)((int)Master.Primary + (byte)nivotrackCfg.Address); data[2] = (byte)Cmd.ReadUniqueID; data[3] = bcnt; data[4] = 0; /// reserved for chehcksum hart.SendMessage(5, data); } void RequestProcessValue() { byte bcnt = (byte)0; /// block count byte[] data = new byte[5]; data[0] = (byte)Mark.ShortFrameMaster2Slave; data[1] = (byte)((int)Master.Primary + (byte)nivotrackCfg.Address); data[2] = (byte)Cmd.ReadPrimaryVar; data[3] = bcnt; data[4] = 0; /// reserved for chehcksum hart.SendMessage(5, data); } /// Run this device public void RunDeviceBefore() { if (nivotrackCfg.DebugLevel == DebugMode.Simulate) return; if (hart.ReceivedTelegrams[nivotrackCfg.Address].Count > 0) { byte[] data = hart.ReceivedTelegrams[nivotrackCfg.Address].Dequeue(); if ((data.Length == 12) && (data[0] == (byte)Mark.ShortFrameSlave2Master) && (data[2] == (byte)Cmd.ReadPrimaryVar) && (data[3] == 7)) { /// /// A frame with a status and a primary variable /// ushort state = (ushort)(256 * data[4] + data[5]); byte unitsID = data[6]; byte[] floatData = new byte[] { data[10], data[9], data[8], data[7] }; rawLevel_mm = System.BitConverter.ToSingle(floatData, 0); isRawLevelValid = true; log.InfoFormat("{0} : Received water level = {1} mm, State = {2}", Name, rawLevel_mm, state.ToString("X4")); } else if ((data.Length == 7) && (data[0] == (byte)Mark.ShortFrameSlave2Master) && (data[2] == (byte)Cmd.ReadPrimaryVar) && (data[3] == 2)) { /// /// A frame with a status only /// ushort state = (ushort)(256 * data[4] + data[5]); isRawLevelValid = false; log.InfoFormat("{0} : State = {1}", Name, state.ToString("X4")); } } } public void RunDeviceAfter() { if (nivotrackCfg.DebugLevel == DebugMode.Simulate) return; if ((StateMachine.Time % 2) == (nivotrackCfg.Address % 2)) { RequestProcessValue(); } } public void StopDevice() { } public void StopDevice2() { } /// /// Gets the most recent raw level measurement and applies correction table /// /// public double ReadLevel() { return Config.Entities.MeasurementCorrection.CorrectedValue(rawLevel_mm, Corrections); } /// Reference to the operation public IOperation ReadLevelOp(ref DoubleBox levelBox) { if (currentOp != CurrentOp.None) throw new Exception("Sequence error"); this.levelBox = levelBox; currentOp = CurrentOp.ReadLevel; return this; } /// Reference to the operation public IOperation ReadStableLevelOp(ref DoubleBox levelBox, double maxSDev) { if (currentOp != CurrentOp.None) throw new Exception("Sequence error"); this.levelBox = levelBox; this.maxSDev = maxSDev; currentOp = CurrentOp.ReadStableLevel; return this; } /// Start this operation public void Start() { if (currentOp == CurrentOp.ReadLevel) { if (isRawLevelValid && (levelBox != null)) levelBox.Val = ReadLevel(); } else if (currentOp == CurrentOp.ReadStableLevel) { currentReadingsCount = 0; measurementCompleted = false; if (isRawLevelValid) { levelReadings[currentReadingsCount++] = ReadLevel(); } } } /// Run this operation /// Event.ResultsPrinted public Event Run() { if (measurementCompleted) { if ((currentOp == CurrentOp.ReadLevel) && (levelBox != null)) levelBox.Val = ReadLevel(); return Event.LevelDone; } if ((currentOp == CurrentOp.ReadLevel) && isRawLevelValid) { if (levelBox != null) levelBox.Val = ReadLevel(); measurementCompleted = true; return Event.LevelDone; } else if ((currentOp == CurrentOp.ReadStableLevel) && isRawLevelValid) { /// /// Read new level and put it into a FIFO buffer /// if (currentReadingsCount < StableReadingsCount) { levelReadings[currentReadingsCount++] = ReadLevel(); } else { /// Shift data in the (already full) buffer, save the mass into the last buffer item for (int i = 1; i < StableReadingsCount; i++) { levelReadings[i - 1] = levelReadings[i]; } levelReadings[StableReadingsCount - 1] = ReadLevel(); } /// /// Evaluate reading stability /// if (currentReadingsCount >= StableReadingsCount) { Array.Copy(levelReadings, sortedLevelReadings, StableReadingsCount); Array.Sort(sortedLevelReadings); if (true) { /// Spread of values (except of the largest and the smalles value) must be <= maxSDev if ((sortedLevelReadings[StableReadingsCount - 2] - sortedLevelReadings[1]) <= maxSDev) { double massSum = 0; for (int i = 1; i < StableReadingsCount - 1; i++) massSum += sortedLevelReadings[i]; double ave = massSum / (StableReadingsCount - 2); if (levelBox != null) levelBox.Val = ave; measurementCompleted = true; log.InfoFormat("ReadStableLevelOp.Run() ... valid level={0} ... returning LevelDone", ave); return Event.LevelDone; } } else { /// Standard deviation (except of the largest and the smalles value) must be <= maxSDev double massSum = 0; for (int i = 1; i < StableReadingsCount - 1; i++) massSum += sortedLevelReadings[i]; double ave = massSum / (StableReadingsCount - 2); double sdev = 0; for (int i = 1; i < StableReadingsCount - 1; i++) sdev += ((sortedLevelReadings[i] - ave) * (sortedLevelReadings[i] - ave)); if (Math.Sqrt(sdev / (StableReadingsCount - 2)) <= maxSDev) { if (levelBox != null) levelBox.Val = ave; measurementCompleted = true; log.InfoFormat("ReadStableMassOp.Run() ... valid mass={0} ... returning Event.BalanceDone", ave); return Event.LevelDone; } } } } return Event.Busy; } /// Stop this operation public void Stop() { currentOp = CurrentOp.None; } } }