using Common; using Config.Entities; using log4net; /// /// Copyright (c) 2021 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.Xml.Serialization; using TBF.Boxes; using TBF.Resources; using TBF.Rig.Generic; namespace TBF.Rig.Modbus.TempMeter.Groch { public class TempMeter : ComponentBase, GenericDevices.ITempMeter, IDevice, GenericDevices.IHasCalendarEvents, SchematicDrawing.IDrawingItCmpntWithMeasuredVal { private static readonly ILog log = LogManager.GetLogger(typeof(TempMeter)); public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } public const int GrochAddress = 3; readonly TempMeterCfg tempMtrCfg; /// int Channel { get { return tempMtrCfg.Channel; } } /// 0 .. 7 (7 = TempMeterCfg.ChannelsCount-1) double A1 { get { return tempMtrCfg.A1; } } double A2 { get { return tempMtrCfg.A2; } } double A3 { get { return tempMtrCfg.A3; } } double A4 { get { return tempMtrCfg.A4; } } double A5 { get { return tempMtrCfg.A5; } } double DefaultTemp { get { return tempMtrCfg != null ? Units.ConvertFrom(tempMtrCfg.MsrdUnit, tempMtrCfg.DefaultTemp) : 0; } } public SchematicDrawing.IDrawingItem DrawingItem { get { return tempMtrCfg as SchematicDrawing.IDrawingItem; } } Common.Modbus modbus; static int[] tempRaw = new int[TempMeterCfg.ChannelsCount]; public bool MsrmntAvailable { get { return true; } } public double MeasuredVal { get { return ReadTemperature(); } } public double MsrdValLimLo { get { return Units.ConvertFrom(MsrdUnit, tempMtrCfg.MsrdValLimLo); } } public double MsrdValLimHi { get { return Units.ConvertFrom(MsrdUnit, tempMtrCfg.MsrdValLimHi); } } public string MsrdFormat { get { return tempMtrCfg.MsrdFormat; } } public Unit MsrdUnit { get { return tempMtrCfg.MsrdUnit; } } public string AltString { get { return "Invalid format"; } } int ticketNumber; /// 0 .. number of devices registered for regular polling - 1 //[XmlIgnore] public bool IsOffline { get { return tempMtrCfg.IsOffline; } set { tempMtrCfg.IsOffline = value; } } // Shared device/channel health tracking (updated by Channel 0) static int grochSilentCycles = 0; static bool grochOffline = true; // Per-channel consecutive invalid-value counters static int[] invalidStreak = new int[TempMeterCfg.ChannelsCount]; // Thresholds const int GrochOfflineAfterCycles = 30; // No valid telegram N cycles => Groch offline const int ChannelOfflineAfterInvalid = 30; // Invalid value N times => channel offline public TempMeter() { } public TempMeter(Generic.IComponentCfg cfg, IList components) : base(cfg) { tempMtrCfg = cfg as TempMeterCfg; } public override void Initialize() { // Ensure shared static health tracking is initialized only once EnsureSharedInitialized(); if (DebugLevel == DebugMode.Normal) { // Each instance starts from shared states (will become online automatically after valid data) bool channelOffline = invalidStreak[Channel] >= ChannelOfflineAfterInvalid; IsOffline = grochOffline || channelOffline; // Only TempMeter with Channel == 0 communicates via the parent Modbus component if (Channel == 0) { modbus = TbfComponents.FindComponent(tempMtrCfg.ParentName) as Common.Modbus; if (modbus == null) throw new Exception("Cannot find " + Name + " parent"); modbus.ComponentNames[0] = Name; ticketNumber = modbus.RegisterForPolling(); } log.FatalFormat("{0} initialized: {1}", Name, this); } else { // In simulation you can decide whether to keep IsOffline false IsOffline = false; log.FatalFormat("{0} simulated: {1}", Name, this); } } public IList GetCalendarEvents() { DateTime calibrationDue = tempMtrCfg.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; } public void RunDeviceBefore() { /*if (DebugLevel == DebugMode.Normal && Channel == 0) { /// Only TempMeter with Channel == 0 communicates via the parent Modbus component /// if (modbus.ReceivedTelegrams[0].Count > 0) { byte[] telegram = modbus.ReceivedTelegrams[0].Dequeue(); if (telegram.Length == 28) { byte checksum = 0; for (int i = 0; i < 27; i++) checksum += telegram[i]; if (telegram[0] == '\r' && telegram[1] == '\n' && telegram[2] == GrochAddress && telegram[3] == 28 && telegram[5] == 2 && telegram[26] == 3 && telegram[27] == checksum) { if (TempMeterCfg.ChannelsCount > 0) tempRaw[0] = telegram[10] + 256 * telegram[11]; if (TempMeterCfg.ChannelsCount > 1) tempRaw[1] = telegram[12] + 256 * telegram[13]; if (TempMeterCfg.ChannelsCount > 2) tempRaw[2] = telegram[14] + 256 * telegram[15]; if (TempMeterCfg.ChannelsCount > 3) tempRaw[3] = telegram[16] + 256 * telegram[17]; if (TempMeterCfg.ChannelsCount > 4) tempRaw[4] = telegram[18] + 256 * telegram[19]; if (TempMeterCfg.ChannelsCount > 5) tempRaw[5] = telegram[20] + 256 * telegram[21]; if (TempMeterCfg.ChannelsCount > 6) tempRaw[6] = telegram[22] + 256 * telegram[23]; if (TempMeterCfg.ChannelsCount > 7) tempRaw[7] = telegram[24] + 256 * telegram[25]; if (TempMeterCfg.ChannelsCount > 7) { log.WarnFormat("Raw temperature values: {0} {1} {2} {3} {4} {5} {6} {7}", tempRaw[0], tempRaw[1], tempRaw[2], tempRaw[3], tempRaw[4], tempRaw[5], tempRaw[6], tempRaw[7]); } } } } }*/ if (DebugLevel != DebugMode.Normal) return; if (Channel == 0) { // update shared state first (process telegrams) UpdateSharedStateFromGroch(); } // now every instance (including ch0) computes its offline state from fresh shared values bool channelOffline = invalidStreak[Channel] >= ChannelOfflineAfterInvalid; IsOffline = grochOffline || channelOffline; } public void RunDeviceAfter() { /// Only TempMeter with Channel == 0 communicates via the parent Modbus component if (DebugLevel == DebugMode.Normal && Channel == 0 && modbus.IsMyTurn(ticketNumber)) { modbus.SendMessageGroch(new byte[] { 0x20, 0x20, 0x20, 0x20, 0x10, 2, 1, 3, 6, 0x16 }, Name); } } public void StopDevice() { } public void StopDevice2() { } public double ReadTemperature() { if (Cfg.DebugLevel == DebugMode.Normal) { double raw = Convert.ToDouble(tempRaw[Channel]); double tempWOCorrection = (((A5 * raw + A4) * raw + A3) * raw + A2) * raw + A1; return MeasurementCorrection.CorrectedValue(tempWOCorrection, Corrections); } else if (Cfg.DebugLevel == DebugMode.Simulate) { return DefaultTemp; } else { return 0; } } // Decide whether the raw value looks valid. // Adjust this to match real device behavior. static bool IsValidRaw(int raw) { if (raw == 0) return false; if (raw == 0xFFFF) return false; return true; } /// /// Events: TempDone, Error /// /// Reference to a variable for the temperature in Celsius /// ReadTempOp instance reference casted to IOperaton public IOperation ReadTempOp(ref DoubleBox temp) { return new ReadTempOp(this, ref temp); } /// /// Events: tempDone, Error /// /// Reference to a variable for the temperature in Celsius /// Event returned when measurement done /// ReadTempOp instance reference casted to IOperaton public IOperation ReadTempOp(ref DoubleBox temp, Event tempDone) { return new ReadTempOp(this, ref temp, tempDone); } // Add to class TempMeter static readonly object initLock = new object(); static bool sharedInitialized = false; static void EnsureSharedInitialized() { if (sharedInitialized) return; lock (initLock) { if (sharedInitialized) return; // Start as offline until we receive the first valid telegram grochSilentCycles = GrochOfflineAfterCycles; grochOffline = true; for (int i = 0; i < TempMeterCfg.ChannelsCount; i++) { invalidStreak[i] = ChannelOfflineAfterInvalid; // start channels as offline until first valid value tempRaw[i] = 0; // optional: reset raw buffer } sharedInitialized = true; } } void UpdateSharedStateFromGroch() { bool gotValidTelegram = false; // Process all queued telegrams to avoid backlog accumulation while (modbus.ReceivedTelegrams[0].Count > 0) { byte[] telegram = modbus.ReceivedTelegrams[0].Dequeue(); if (telegram.Length != 28) continue; byte checksum = 0; for (int i = 0; i < 27; i++) checksum += telegram[i]; bool isValidGroch = telegram[0] == '\r' && telegram[1] == '\n' && telegram[2] == GrochAddress && telegram[3] == 28 && telegram[5] == 2 && telegram[26] == 3 && telegram[27] == checksum; if (!isValidGroch) continue; // At least one valid telegram received this cycle gotValidTelegram = true; // Extract raw channel values if (TempMeterCfg.ChannelsCount > 0) { int raw = telegram[10] + 256 * telegram[11]; tempRaw[0] = raw; invalidStreak[0] = IsValidRaw(raw) ? 0 : invalidStreak[0] + 1; } if (TempMeterCfg.ChannelsCount > 1) { int raw = telegram[12] + 256 * telegram[13]; tempRaw[1] = raw; invalidStreak[1] = IsValidRaw(raw) ? 0 : invalidStreak[1] + 1; } if (TempMeterCfg.ChannelsCount > 2) { int raw = telegram[14] + 256 * telegram[15]; tempRaw[2] = raw; invalidStreak[2] = IsValidRaw(raw) ? 0 : invalidStreak[2] + 1; } if (TempMeterCfg.ChannelsCount > 3) { int raw = telegram[16] + 256 * telegram[17]; tempRaw[3] = raw; invalidStreak[3] = IsValidRaw(raw) ? 0 : invalidStreak[3] + 1; } if (TempMeterCfg.ChannelsCount > 4) { int raw = telegram[18] + 256 * telegram[19]; tempRaw[4] = raw; invalidStreak[4] = IsValidRaw(raw) ? 0 : invalidStreak[4] + 1; } if (TempMeterCfg.ChannelsCount > 5) { int raw = telegram[20] + 256 * telegram[21]; tempRaw[5] = raw; invalidStreak[5] = IsValidRaw(raw) ? 0 : invalidStreak[5] + 1; } if (TempMeterCfg.ChannelsCount > 6) { int raw = telegram[22] + 256 * telegram[23]; tempRaw[6] = raw; invalidStreak[6] = IsValidRaw(raw) ? 0 : invalidStreak[6] + 1; } if (TempMeterCfg.ChannelsCount > 7) { int raw = telegram[24] + 256 * telegram[25]; tempRaw[7] = raw; invalidStreak[7] = IsValidRaw(raw) ? 0 : invalidStreak[7] + 1; } } // Device-level offline detection if (gotValidTelegram) { grochSilentCycles = 0; grochOffline = false; } else { grochSilentCycles++; if (grochSilentCycles >= GrochOfflineAfterCycles) { grochOffline = true; } } } } }