diff --git a/SchematicDrawing/IDrawingItemWithMeasuredVal.cs b/SchematicDrawing/IDrawingItemWithMeasuredVal.cs index c9725b24d..5ad98258b 100644 --- a/SchematicDrawing/IDrawingItemWithMeasuredVal.cs +++ b/SchematicDrawing/IDrawingItemWithMeasuredVal.cs @@ -13,5 +13,6 @@ namespace SchematicDrawing Common.Unit MsrdUnit { get; } double MsrdValLimLo { get; } double MsrdValLimHi { get; } + bool IsOffline { get; set; } } } diff --git a/SchematicDrawing/SchematicDrawingCtrl.cs b/SchematicDrawing/SchematicDrawingCtrl.cs index 5c6c4eb66..a09314209 100644 --- a/SchematicDrawing/SchematicDrawingCtrl.cs +++ b/SchematicDrawing/SchematicDrawingCtrl.cs @@ -672,7 +672,11 @@ namespace SchematicDrawing SizeF size = e.Graphics.MeasureString(strVal, measuredValueFont); Rectangle rect = new Rectangle(itm.X + itm.MsrdX, itm.Y + itm.MsrdY, (int)Math.Round(size.Width) + 4, (int)Math.Round(size.Height) + 2); ItemElementRectangles.Add(new ItemElementRect(item, Element.MeasuredVal, rect)); - if (measuredValues[msrdValIx] >= itm.MsrdValLimLo && measuredValues[msrdValIx] <= itm.MsrdValLimHi) + if ((item as IDrawingItemWithMeasuredVal).IsOffline) + { + e.Graphics.FillRectangle(measuredValueErrorBackColor, rect); + } + else if (measuredValues[msrdValIx] >= itm.MsrdValLimLo && measuredValues[msrdValIx] <= itm.MsrdValLimHi) { e.Graphics.FillRectangle(measuredValueBackColor, rect); } diff --git a/SharedComponents/obj/SharedComponents.csproj.nuget.g.props b/SharedComponents/obj/SharedComponents.csproj.nuget.g.props index 568c2629e..d780aba7f 100644 --- a/SharedComponents/obj/SharedComponents.csproj.nuget.g.props +++ b/SharedComponents/obj/SharedComponents.csproj.nuget.g.props @@ -5,11 +5,12 @@ NuGet $(MSBuildThisFileDirectory)project.assets.json $(UserProfile)\.nuget\packages\ - C:\Users\micha\.nuget\packages\ + C:\Users\MFRNIAK\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 7.0.0 + 6.14.0 - + + \ No newline at end of file diff --git a/TBF/Rig/Ambient/Comet/AmbientCfg.cs b/TBF/Rig/Ambient/Comet/AmbientCfg.cs index 80a74662c..44cc4148b 100644 --- a/TBF/Rig/Ambient/Comet/AmbientCfg.cs +++ b/TBF/Rig/Ambient/Comet/AmbientCfg.cs @@ -73,8 +73,11 @@ namespace TBF.Rig.Ambient.Comet [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - AmbientCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + AmbientCfg() { GNodes = new List(); Format = string.Empty; diff --git a/TBF/Rig/Ambient/Greco/AmbientCfg.cs b/TBF/Rig/Ambient/Greco/AmbientCfg.cs index df954c7a0..43ad26684 100644 --- a/TBF/Rig/Ambient/Greco/AmbientCfg.cs +++ b/TBF/Rig/Ambient/Greco/AmbientCfg.cs @@ -72,6 +72,9 @@ namespace TBF.Rig.Ambient.Greco [XmlIgnore] public IList GNodes { get; set; } + [XmlIgnore] + public bool IsOffline { get; set; } + /// Private parameterless constructor invoked by all other (public) constructors AmbientCfg() { diff --git a/TBF/Rig/ControlBoard/Uni/UniCBCfg.cs b/TBF/Rig/ControlBoard/Uni/UniCBCfg.cs index 1dbfb12d8..066bf810e 100644 --- a/TBF/Rig/ControlBoard/Uni/UniCBCfg.cs +++ b/TBF/Rig/ControlBoard/Uni/UniCBCfg.cs @@ -66,6 +66,9 @@ namespace TBF.Rig.ControlBoard.Uni [XmlIgnore] public IList EdgesToSet { get; set; } + [XmlIgnore] + public bool IsOffline { get; set; } + /// Private parameterless constructor invoked by all other (public) constructors UniCBCfg() { diff --git a/TBF/Rig/Dummy/FlowMeter/FlowMeterCfg.cs b/TBF/Rig/Dummy/FlowMeter/FlowMeterCfg.cs index a2ba2eebb..8981a7396 100644 --- a/TBF/Rig/Dummy/FlowMeter/FlowMeterCfg.cs +++ b/TBF/Rig/Dummy/FlowMeter/FlowMeterCfg.cs @@ -54,9 +54,12 @@ namespace TBF.Rig.Dummy.FlowMeter double msrdValLimLo = 0; double msrdValLimHi; + [XmlIgnore] + public bool IsOffline { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - FlowMeterCfg() + + /// Private parameterless constructor invoked by all other (public) constructors + FlowMeterCfg() { GNodes = new List(); } diff --git a/TBF/Rig/Keithley/TempMeter/TempMeter.cs b/TBF/Rig/Keithley/TempMeter/TempMeter.cs index f2186e7bd..379923568 100644 --- a/TBF/Rig/Keithley/TempMeter/TempMeter.cs +++ b/TBF/Rig/Keithley/TempMeter/TempMeter.cs @@ -1,18 +1,19 @@ -/// +using Common; +using log4net; +using SchematicDrawing; +/// /// Copyright (c) 2016-2020 Sensus Metering Systems /// using System; using System.Collections.Generic; -using Common; -using log4net; using TBF.Boxes; -using TBF.Rig.Keithley.Multimeter_2010_RS232; using TBF.Resources; +using TBF.Rig.Keithley.Multimeter_2010_RS232; namespace TBF.Rig.Keithley.TempMeter { - public class TempMeter : ComponentBase, GenericDevices.ITempMeter, GenericDevices.IHasCalendarEvents, SchematicDrawing.IDrawingItCmpnt - { + public class TempMeter : ComponentBase, GenericDevices.ITempMeter, GenericDevices.IHasCalendarEvents, IDrawingItCmpntWithMeasuredVal + { private static readonly ILog log = LogManager.GetLogger(typeof(TempMeter)); public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } @@ -36,9 +37,17 @@ namespace TBF.Rig.Keithley.TempMeter /// Dictionaries to maintain separate operations for each FloatBox argument Dictionary operations; Dictionary operations2; + public bool IsOffline + { + get { return tempMtrCfg.IsOffline; } + set { tempMtrCfg.IsOffline = value; } + } + + int invalidStreak = 0; + const int OfflineAfterInvalid = 30; - public TempMeter() { } + public TempMeter() { } public TempMeter(Generic.IComponentCfg cfg, IList components) : base(cfg) @@ -93,39 +102,66 @@ namespace TBF.Rig.Keithley.TempMeter public double ReadTemperature() { - double resistance = Multimeter.ReadResistance(tempMtrCfg.Channel); + // In non-Normal modes do not perform offline detection here + if (Cfg.DebugLevel != DebugMode.Normal) + { + if (Cfg.DebugLevel == DebugMode.Simulate) + return 20.0; + return 0; + } - double temperature = 0; - if (resistance > 0 && resistance < 2000) - { - if (tempMtrCfg.UseITS90) - { - temperature = Formulas.PlatinumResistanceTM_ITS90_R2T(resistance, - tempMtrCfg.R001C, - tempMtrCfg.a7, - tempMtrCfg.b7, - tempMtrCfg.c7); - } - else - { - temperature = Formulas.PlatinumResistanceTM_ITS27_R2T(resistance, - tempMtrCfg.R0, - tempMtrCfg.A, - tempMtrCfg.B); - } + double resistance; - log.DebugFormat("Ch = {0}, R = {1} Ohm, T = {2} °C", Channel, resistance.ToString("F3"), temperature.ToString("F3")); - } + try + { + resistance = Multimeter.ReadResistance(tempMtrCfg.Channel); + } + catch (Exception ex) + { + invalidStreak++; - return temperature; + if (invalidStreak >= OfflineAfterInvalid) + IsOffline = true; + + log.WarnFormat("Keithley TempMeter {0} ch={1}: ReadResistance failed ({2}/{3}): {4}", + Name, Channel, invalidStreak, OfflineAfterInvalid, ex.Message); + + return 0; // or keep last value, or double.NaN + } + + // Valid resistance => online + invalidStreak = 0; + IsOffline = false; + + double temperature; + if (tempMtrCfg.UseITS90) + { + temperature = Formulas.PlatinumResistanceTM_ITS90_R2T(resistance, + tempMtrCfg.R001C, + tempMtrCfg.a7, + tempMtrCfg.b7, + tempMtrCfg.c7); + } + else + { + temperature = Formulas.PlatinumResistanceTM_ITS27_R2T(resistance, + tempMtrCfg.R0, + tempMtrCfg.A, + tempMtrCfg.B); + } + + log.DebugFormat("Ch = {0}, R = {1} Ohm, T = {2} °C", + Channel, resistance.ToString("F3"), temperature.ToString("F3")); + + return temperature; } - /// - /// Events: TempDone, Error - /// - /// Reference to a variable for the temperature in Celsius - /// ReadTempOp instance reference casted to IOperaton - public IOperation ReadTempOp(ref DoubleBox temp) + /// + /// Events: TempDone, Error + /// + /// Reference to a variable for the temperature in Celsius + /// ReadTempOp instance reference casted to IOperaton + public IOperation ReadTempOp(ref DoubleBox temp) { IOperation operation; if (operations.TryGetValue(temp, out operation)) diff --git a/TBF/Rig/Keithley/TempMeter/TempMeterCfg.cs b/TBF/Rig/Keithley/TempMeter/TempMeterCfg.cs index 012327ce0..04539c05f 100644 --- a/TBF/Rig/Keithley/TempMeter/TempMeterCfg.cs +++ b/TBF/Rig/Keithley/TempMeter/TempMeterCfg.cs @@ -62,8 +62,11 @@ namespace TBF.Rig.Keithley.TempMeter [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - TempMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + TempMeterCfg() { GNodes = new List(); } diff --git a/TBF/Rig/MettlerToledo/Standard/BalanceCfg.cs b/TBF/Rig/MettlerToledo/Standard/BalanceCfg.cs index 07d5eec2b..11fda0d79 100644 --- a/TBF/Rig/MettlerToledo/Standard/BalanceCfg.cs +++ b/TBF/Rig/MettlerToledo/Standard/BalanceCfg.cs @@ -81,9 +81,12 @@ namespace TBF.Rig.MettlerToledo.Standard [XmlIgnore] public double MsrdValLimHi { get { return Capacity; } set { } } + [XmlIgnore] + public bool IsOffline { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - BalanceCfg() + + /// Private parameterless constructor invoked by all other (public) constructors + BalanceCfg() { GNodes = new List(); } diff --git a/TBF/Rig/Modbus/Ambient/Comet/AmbientCfg.cs b/TBF/Rig/Modbus/Ambient/Comet/AmbientCfg.cs index f67e874df..4f71c437b 100644 --- a/TBF/Rig/Modbus/Ambient/Comet/AmbientCfg.cs +++ b/TBF/Rig/Modbus/Ambient/Comet/AmbientCfg.cs @@ -68,8 +68,11 @@ namespace TBF.Rig.Modbus.Ambient.Comet [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - AmbientCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + AmbientCfg() { GNodes = new List(); Format = string.Empty; diff --git a/TBF/Rig/Modbus/Meret/AdjustableScale/AdjustableMeterCfg.cs b/TBF/Rig/Modbus/Meret/AdjustableScale/AdjustableMeterCfg.cs index 3a141c5a4..07b782bfd 100644 --- a/TBF/Rig/Modbus/Meret/AdjustableScale/AdjustableMeterCfg.cs +++ b/TBF/Rig/Modbus/Meret/AdjustableScale/AdjustableMeterCfg.cs @@ -61,8 +61,11 @@ namespace TBF.Rig.Modbus.Meret.AdjustableScale [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - AdjustableMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + AdjustableMeterCfg() { GNodes = new List(); } diff --git a/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeter.cs b/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeter.cs index 045cb5c47..3e6e06f98 100644 --- a/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeter.cs +++ b/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeter.cs @@ -1,15 +1,17 @@ -/// +using AppDiagnostic; +using Common; +using Config.Entities; +using log4net; +using SchematicDrawing; +using SharedComponents; +/// /// Copyright (c) 2016-2020 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; -using log4net; -using Common; -using Config.Entities; -using SchematicDrawing; -using TBF.Rig.Generic; using TBF.Boxes; using TBF.Resources; +using TBF.Rig.Generic; using TBF.Rig.GenericDevices; namespace TBF.Rig.Modbus.PressureMeter.Meret @@ -35,9 +37,17 @@ namespace TBF.Rig.Modbus.PressureMeter.Meret public string AltString { get { return string.Empty; } } int ticketNumber; /// 0 .. number of devices registered for regular polling - 1 + public bool IsOffline + { + get { return pressureMtrCfg.IsOffline; } + set { pressureMtrCfg.IsOffline = value; } + } + + int silentCycles = 0; + const int OfflineAfterSilentCycles = 30; - public PressureMeter() { } + public PressureMeter() { } public PressureMeter(Generic.IComponentCfg cfg, IList components) : base(cfg) @@ -59,6 +69,9 @@ namespace TBF.Rig.Modbus.PressureMeter.Meret modbus.ComponentNames[pressureMtrCfg.ModbusAddress] = Name; ticketNumber = modbus.RegisterForPolling(); log.FatalFormat("{0} initialized: {1}", Name, this); + + IsOffline = true; + silentCycles = OfflineAfterSilentCycles; } else { @@ -68,25 +81,85 @@ namespace TBF.Rig.Modbus.PressureMeter.Meret public void RunDeviceBefore() { - if (DebugLevel == DebugMode.Normal && modbus.ReceivedTelegrams[pressureMtrCfg.ModbusAddress].Count > 0) - { - byte[] telegram = modbus.ReceivedTelegrams[pressureMtrCfg.ModbusAddress].Dequeue(); - if (telegram.Length == 9 && telegram[1] == 4 && telegram[2] == 4) - { - /// Swap byte order - byte t1 = telegram[3]; - byte t2 = telegram[4]; - byte t3 = telegram[5]; - byte t4 = telegram[6]; - telegram[3] = t4; - telegram[4] = t3; - telegram[5] = t2; - telegram[6] = t1; + if (DebugLevel != DebugMode.Normal) + return; - receivedPressure = Units.ConvertFrom(Unit.kPa, System.BitConverter.ToSingle(telegram, 3)); + bool gotValidResponse = false; + + if (DebugLevel == DebugMode.Normal && modbus.ReceivedTelegrams[pressureMtrCfg.ModbusAddress].Count > 0) + { + byte[] telegram = modbus.ReceivedTelegrams[pressureMtrCfg.ModbusAddress].Dequeue(); + if (telegram.Length == 9 && telegram[1] == 4 && telegram[2] == 4) + { + /// Swap byte order + byte t1 = telegram[3]; + byte t2 = telegram[4]; + byte t3 = telegram[5]; + byte t4 = telegram[6]; + telegram[3] = t4; + telegram[4] = t3; + telegram[5] = t2; + telegram[6] = t1; + + receivedPressure = Units.ConvertFrom(Unit.kPa, System.BitConverter.ToSingle(telegram, 3)); log.WarnFormat("Pressure: {0}={1}bar", Name, receivedPressure.ToString("F3")); + + gotValidResponse = true; } - } + else + { + string telHex = telegram == null ? "" : BitConverter.ToString(telegram); + + string reason; + if (telegram == null) reason = "null telegram"; + else if (telegram.Length != 9) reason = $"unexpected length (expected 9, got {telegram.Length})"; + else if (telegram.Length > 1 && telegram[1] != 4) reason = $"unexpected function (expected 0x04, got 0x{telegram[1]:X2})"; + else if (telegram.Length > 2 && telegram[2] != 4) reason = $"unexpected bytecount (expected 0x04, got 0x{telegram[2]:X2})"; + else reason = "unknown mismatch"; + + //LiveLogCache.Instance.AddLog(string.Format( + // ">> RunDeviceBefore() >> component: {0} bad received telegram: {1} data={2}", + // Name, + // reason, + // telHex)); + } + } + else + { + bool debugOk = (DebugLevel == DebugMode.Normal); + int addr = pressureMtrCfg.ModbusAddress; + + try + { + var q = modbus.ReceivedTelegrams[addr]; + int qCount = (q == null) ? -1 : q.Count; + + //LiveLogCache.Instance.AddLog(string.Format( + // ">> RunDeviceBefore() >> component: {0} condition failed | DebugOk={1} | Addr={2} | QueueNull={3} | QueueCount={4} | silentCycles={5}", + // Name, debugOk, addr, (q == null), qCount, silentCycles)); + } + catch (Exception ex) + { + //LiveLogCache.Instance.AddLog(string.Format( + // ">> RunDeviceBefore() >> component: {0} condition failed | DebugOk={1} | Addr={2} | silentCycles={3} | EXCEPTION={4}", + // Name, debugOk, addr, silentCycles, ex.Message)); + } + } + + if (gotValidResponse) + { + silentCycles = 0; + IsOffline = false; + } + else + { + silentCycles++; + if (silentCycles >= OfflineAfterSilentCycles) + { + IsOffline = true; + //LiveLogCache.Instance.AddLog(string.Format(">> RunDeviceBefore() >> component: {0} IsOffline", Name)); + } + } } public void RunDeviceAfter() diff --git a/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeterCfg.cs b/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeterCfg.cs index 1e62b6536..04abe150c 100644 --- a/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeterCfg.cs +++ b/TBF/Rig/Modbus/PressureMeter/Meret/PressureMeterCfg.cs @@ -57,8 +57,11 @@ namespace TBF.Rig.Modbus.PressureMeter.Meret [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - PressureMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + PressureMeterCfg() { GNodes = new List(); } @@ -93,7 +96,7 @@ namespace TBF.Rig.Modbus.PressureMeter.Meret "Modbus address", /// 0 "Default pressure", /// 1 "Display format", /// 2 - "Unit of pressure", /// 3 + "Units of pressure", /// 3 "Pressure limit Lo", /// 4 "Pressure limit Hi", /// 5 }; diff --git a/TBF/Rig/Modbus/TempControl/Easytherm/TControlCfg.cs b/TBF/Rig/Modbus/TempControl/Easytherm/TControlCfg.cs index 915fe6847..ae6eff39c 100644 --- a/TBF/Rig/Modbus/TempControl/Easytherm/TControlCfg.cs +++ b/TBF/Rig/Modbus/TempControl/Easytherm/TControlCfg.cs @@ -59,8 +59,11 @@ namespace TBF.Rig.Modbus.TempControl.Easytherm [XmlIgnore] public IList GNodes { get; set; } - /// Procedure parameters - [XmlIgnore] + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Procedure parameters + [XmlIgnore] public ProcParams ProcParams; public override IParamsProvider GetRuntimeProcParamsProvider() { return ProcParams; } public override IParamsProvider CreateProcParamsProvider() { return new ProcParams(true); } diff --git a/TBF/Rig/Modbus/TempControl/Novus/TControlCfg.cs b/TBF/Rig/Modbus/TempControl/Novus/TControlCfg.cs index 03ad183f4..8958a0e4b 100644 --- a/TBF/Rig/Modbus/TempControl/Novus/TControlCfg.cs +++ b/TBF/Rig/Modbus/TempControl/Novus/TControlCfg.cs @@ -68,9 +68,12 @@ namespace TBF.Rig.Modbus.TempControl.Novus [XmlIgnore] public IList GNodes { get; set; } + [XmlIgnore] + public bool IsOffline { get; set; } - /// Procedure parameters - [XmlIgnore] + + /// Procedure parameters + [XmlIgnore] public ProcParams ProcParams; public override IParamsProvider GetRuntimeProcParamsProvider() { return ProcParams; } public override IParamsProvider CreateProcParamsProvider() { return new ProcParams(true); } diff --git a/TBF/Rig/Modbus/TempMeter/Groch/TempMeter.cs b/TBF/Rig/Modbus/TempMeter/Groch/TempMeter.cs index ec28b1e4c..7a082bebd 100644 --- a/TBF/Rig/Modbus/TempMeter/Groch/TempMeter.cs +++ b/TBF/Rig/Modbus/TempMeter/Groch/TempMeter.cs @@ -1,14 +1,15 @@ -/// +using Common; +using Config.Entities; +using log4net; +/// /// Copyright (c) 2021 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; -using log4net; -using Common; -using Config.Entities; -using TBF.Rig.Generic; +using System.Xml.Serialization; using TBF.Boxes; using TBF.Resources; +using TBF.Rig.Generic; namespace TBF.Rig.Modbus.TempMeter.Groch { @@ -46,6 +47,24 @@ namespace TBF.Rig.Modbus.TempMeter.Groch 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() { } @@ -57,9 +76,17 @@ namespace TBF.Rig.Modbus.TempMeter.Groch public override void Initialize() { + + // Ensure shared static health tracking is initialized only once + EnsureSharedInitialized(); + if (DebugLevel == DebugMode.Normal) { - /// Only TempMeter with Channel == 0 communicates via the parent Modbus component + // 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; @@ -68,10 +95,13 @@ namespace TBF.Rig.Modbus.TempMeter.Groch 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); } } @@ -110,10 +140,11 @@ namespace TBF.Rig.Modbus.TempMeter.Groch public void RunDeviceBefore() { - if (DebugLevel == DebugMode.Normal && Channel == 0) + /*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(); @@ -147,7 +178,20 @@ namespace TBF.Rig.Modbus.TempMeter.Groch } } } + }*/ + + 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() @@ -182,12 +226,21 @@ namespace TBF.Rig.Modbus.TempMeter.Groch } } - /// - /// Events: TempDone, Error - /// - /// Reference to a variable for the temperature in Celsius - /// ReadTempOp instance reference casted to IOperaton - public IOperation ReadTempOp(ref DoubleBox temp) + // 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); } @@ -202,5 +255,137 @@ namespace TBF.Rig.Modbus.TempMeter.Groch { 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; + } + } + } + } } diff --git a/TBF/Rig/Modbus/TempMeter/Groch/TempMeterCfg.cs b/TBF/Rig/Modbus/TempMeter/Groch/TempMeterCfg.cs index cbc2708c7..a51a7aa7f 100644 --- a/TBF/Rig/Modbus/TempMeter/Groch/TempMeterCfg.cs +++ b/TBF/Rig/Modbus/TempMeter/Groch/TempMeterCfg.cs @@ -14,9 +14,9 @@ namespace TBF.Rig.Modbus.TempMeter.Groch { public class TempMeterCfg : ComponentCfgBase, IChildComponentCfg, GenericDevices.ICalibInfoCfg, IParamsProvider, IDrawingItemWithMeasuredVal - { - public static XmlSerializer Serializer = XmlSerializer.FromTypes(new[] { typeof(TempMeterCfg) })[0]; - public override XmlSerializer GetSerializer() { return Serializer; } + { + public static XmlSerializer Serializer = XmlSerializer.FromTypes(new[] { typeof(TempMeterCfg) })[0]; + public override XmlSerializer GetSerializer() { return Serializer; } public IComponentCfgCtrl GetControl(IList cmpntEntities) { @@ -26,9 +26,9 @@ namespace TBF.Rig.Modbus.TempMeter.Groch public const int ChannelsCount = 8; - /// - /// Serialized parameters - /// + /// + /// Serialized parameters + /// public int Address; /// 0 0 .. 255 public int Channel; /// 1 0 .. 7 (7 = TempMeterCfg.ChannelsCount-1) public double A1; /// 2 @@ -65,8 +65,11 @@ namespace TBF.Rig.Modbus.TempMeter.Groch [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - TempMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + TempMeterCfg() { GNodes = new List(); } @@ -113,7 +116,7 @@ namespace TBF.Rig.Modbus.TempMeter.Groch "A5", /// 6 "Default temperature", /// 7 "Display format", /// 8 - "Unit of temperature", /// 9 + "Units of temperature", /// 9 "Temperature limit Lo", /// 10 "Temperature limit Hi", /// 11 }; diff --git a/TBF/Rig/Modbus/TempMeter/Meret/TempMeter.cs b/TBF/Rig/Modbus/TempMeter/Meret/TempMeter.cs index b27b2983e..edb77b76a 100644 --- a/TBF/Rig/Modbus/TempMeter/Meret/TempMeter.cs +++ b/TBF/Rig/Modbus/TempMeter/Meret/TempMeter.cs @@ -33,9 +33,17 @@ namespace TBF.Rig.Modbus.TempMeter.Meret public string AltString { get { return string.Empty; } } int ticketNumber; /// 0 .. number of devices registered for regular polling - 1 + public bool IsOffline + { + get { return tempMtrCfg.IsOffline; } + set { tempMtrCfg.IsOffline = value; } + } + + int silentCycles = 0; + const int OfflineAfterSilentCycles = 30; // e.g. 5 cycles without valid response - public TempMeter() { } + public TempMeter() { } public TempMeter(Generic.IComponentCfg cfg, IList components) : base(cfg) @@ -50,6 +58,11 @@ namespace TBF.Rig.Modbus.TempMeter.Meret modbus.ComponentNames[tempMtrCfg.ModbusAddress] = Name; ticketNumber = modbus.RegisterForPolling(); + + // Start offline until first valid response arrives (optional but consistent) + IsOffline = true; + silentCycles = OfflineAfterSilentCycles; + log.FatalFormat("{0} initialized: {1}", Name, this); } @@ -59,25 +72,45 @@ namespace TBF.Rig.Modbus.TempMeter.Meret /// Run this device public void RunDeviceBefore() { - if (modbus.ReceivedTelegrams[tempMtrCfg.ModbusAddress].Count > 0) - { - byte[] telegram = modbus.ReceivedTelegrams[tempMtrCfg.ModbusAddress].Dequeue(); - if (telegram.Length == 9 && telegram[1] == 4 && telegram[2] == 4) - { - /// Swap byte order - byte t1 = telegram[3]; - byte t2 = telegram[4]; - byte t3 = telegram[5]; - byte t4 = telegram[6]; - telegram[3] = t4; - telegram[4] = t3; - telegram[5] = t2; - telegram[6] = t1; + if (Cfg.DebugLevel != DebugMode.Normal) + return; - receivedTemp = System.BitConverter.ToSingle(telegram, 3); - log.WarnFormat("Temperature: {0}={1}°C", Name, receivedTemp.ToString("F1")); - } - } + bool gotValidResponse = false; + + while (modbus.ReceivedTelegrams[tempMtrCfg.ModbusAddress].Count > 0) + { + byte[] telegram = modbus.ReceivedTelegrams[tempMtrCfg.ModbusAddress].Dequeue(); + + // Presence = valid response frame + if (telegram.Length == 9 && telegram[1] == 4 && telegram[2] == 4) + { + gotValidResponse = true; + + // Optional: still parse temperature for display/use (does not affect presence) + byte t1 = telegram[3]; + byte t2 = telegram[4]; + byte t3 = telegram[5]; + byte t4 = telegram[6]; + telegram[3] = t4; + telegram[4] = t3; + telegram[5] = t2; + telegram[6] = t1; + + receivedTemp = BitConverter.ToSingle(telegram, 3); + } + } + + if (gotValidResponse) + { + silentCycles = 0; + IsOffline = false; + } + else + { + silentCycles++; + if (silentCycles >= OfflineAfterSilentCycles) + IsOffline = true; + } } public void RunDeviceAfter() @@ -116,6 +149,17 @@ namespace TBF.Rig.Modbus.TempMeter.Meret } } + static bool IsValidTemperature(float t) + { + // Reject NaN/Infinity + if (float.IsNaN(t) || float.IsInfinity(t)) return false; + + // Put a reasonable physical range here (adjust to your real use case) + if (t < -50.0f || t > 200.0f) return false; + + return true; + } + /// /// Events: PressureDone, Error /// diff --git a/TBF/Rig/Modbus/TempMeter/Meret/TempMeterCfg.cs b/TBF/Rig/Modbus/TempMeter/Meret/TempMeterCfg.cs index 417f9c743..e69fe3f67 100644 --- a/TBF/Rig/Modbus/TempMeter/Meret/TempMeterCfg.cs +++ b/TBF/Rig/Modbus/TempMeter/Meret/TempMeterCfg.cs @@ -58,8 +58,11 @@ namespace TBF.Rig.Modbus.TempMeter.Meret [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - TempMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + TempMeterCfg() { GNodes = new List(); } @@ -94,7 +97,7 @@ namespace TBF.Rig.Modbus.TempMeter.Meret "Modbus address", /// 0 "Default temperature", /// 1 "Display format", /// 2 - "Unit of temperature", /// 3 + "Units of temperature", /// 3 "Temperature limit Lo", /// 4 "Temperature limit Hi", /// 5 }; diff --git a/TBF/Rig/Modbus/UltrasoundLevelMeter/LevelMeterCfg.cs b/TBF/Rig/Modbus/UltrasoundLevelMeter/LevelMeterCfg.cs index f60af8ebd..fa56ddec1 100644 --- a/TBF/Rig/Modbus/UltrasoundLevelMeter/LevelMeterCfg.cs +++ b/TBF/Rig/Modbus/UltrasoundLevelMeter/LevelMeterCfg.cs @@ -49,8 +49,11 @@ namespace TBF.Rig.Modbus.UltrasoundLevelMeter [XmlIgnore] public IList GNodes { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - LevelMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + LevelMeterCfg() { GNodes = new List(); } diff --git a/TBF/Rig/Scales/MettlerToledo/ScaleCfg.cs b/TBF/Rig/Scales/MettlerToledo/ScaleCfg.cs index 0a1e18117..c9faf3433 100644 --- a/TBF/Rig/Scales/MettlerToledo/ScaleCfg.cs +++ b/TBF/Rig/Scales/MettlerToledo/ScaleCfg.cs @@ -91,9 +91,12 @@ namespace TBF.Rig.Scales.MettlerToledo [XmlIgnore] public double MsrdValLimHi { get { return Capacity; } set { } } + [XmlIgnore] + public bool IsOffline { get; set; } - /// Private parameterless constructor invoked by all other (public) constructors - ScaleCfg() + + /// Private parameterless constructor invoked by all other (public) constructors + ScaleCfg() { GNodes = new List(); } diff --git a/TBF/Rig/Uni/FlowMeter/FlowMeterCfg.cs b/TBF/Rig/Uni/FlowMeter/FlowMeterCfg.cs index 7aef11c07..71b99473d 100644 --- a/TBF/Rig/Uni/FlowMeter/FlowMeterCfg.cs +++ b/TBF/Rig/Uni/FlowMeter/FlowMeterCfg.cs @@ -117,6 +117,9 @@ namespace TBF.Rig.Uni.FlowMeter double msrdValLimLo = 0; /// in [m3/h] double msrdValLimHi; /// in [m3/h] + [XmlIgnore] + public bool IsOffline { get; set; } + public bool RangeEnabled(int rangeIx1) { diff --git a/TBF/Rig/Uni/FlowMetersInParallel/FlowMeterCfg.cs b/TBF/Rig/Uni/FlowMetersInParallel/FlowMeterCfg.cs index f8244bad1..b449a2ea6 100644 --- a/TBF/Rig/Uni/FlowMetersInParallel/FlowMeterCfg.cs +++ b/TBF/Rig/Uni/FlowMetersInParallel/FlowMeterCfg.cs @@ -58,8 +58,11 @@ namespace TBF.Rig.Uni.FlowMetersInParallel private IEnumerable flowMeters; - /// Private parameterless constructor invoked by all other (public) constructors - FlowMeterCfg() + [XmlIgnore] + public bool IsOffline { get; set; } + + /// Private parameterless constructor invoked by all other (public) constructors + FlowMeterCfg() { GNodes = new List(); } diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 062ee596a..74a3c8668 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1456,7 +1456,9 @@ - + + Component +