diff --git a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs index fa4ca16..b8892bc 100644 --- a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs +++ b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs @@ -18,8 +18,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService private const ushort OrificeTemperatureRegister = 30; private const ushort SampleTemperatureRegister = 36; private const ushort AbsorbanceRegister = 120; - private const double AbsorbanceStableTolerance = 0.25; - private const int AbsorbanceStableReadCount = 2; private const ushort CFactorRegister = 312; private const ushort HeatReleaseRateRegister = 354; private const ushort PeakHeatReleaseRateRegister = 376; @@ -44,9 +42,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService private readonly HashSet _loggedInvalidFloatDiagnostics = []; private readonly Dictionary _stableRealtimeValues = []; private readonly Dictionary _pendingZeroReadCounts = []; - private double? _stableAbsorbance; - private double? _pendingAbsorbance; - private int _pendingAbsorbanceReadCount; + private double? _lastAbsorbanceValue; private ModbusFloatByteOrder? _preferredFloatByteOrder; public ModbusRealtimeDataService( @@ -113,8 +109,14 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService private double ReadAbsorbanceOrEmpty() { var value = ReadRangedFloatOrEmpty("Absorbance", AbsorbanceRegister, 0, 100); - return double.IsFinite(value) && TryUpdateStableAbsorbance(value, out var stableValue) - ? stableValue + if (double.IsFinite(value)) + { + _lastAbsorbanceValue = value; + return value; + } + + return _lastAbsorbanceValue.HasValue + ? _lastAbsorbanceValue.Value : double.NaN; } @@ -126,41 +128,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService : double.NaN; } - private bool TryUpdateStableAbsorbance(double value, out double stableValue) - { - stableValue = _stableAbsorbance ?? value; - - if (_stableAbsorbance.HasValue - && Math.Abs(value - _stableAbsorbance.Value) <= AbsorbanceStableTolerance) - { - _pendingAbsorbance = null; - _pendingAbsorbanceReadCount = 0; - stableValue = _stableAbsorbance.Value; - return true; - } - - if (!_pendingAbsorbance.HasValue - || Math.Abs(value - _pendingAbsorbance.Value) > AbsorbanceStableTolerance) - { - _pendingAbsorbance = value; - _pendingAbsorbanceReadCount = 1; - return false; - } - - _pendingAbsorbanceReadCount++; - - if (_pendingAbsorbanceReadCount < AbsorbanceStableReadCount) - { - return false; - } - - _stableAbsorbance = value; - _pendingAbsorbance = null; - _pendingAbsorbanceReadCount = 0; - stableValue = value; - return true; - } - private bool TrySelectRealtimeFloat( ModbusFloatReadResult result, double minimum, diff --git a/ConeCalorimeter/ViewModels/RealtimeDataRowViewModel.cs b/ConeCalorimeter/ViewModels/RealtimeDataRowViewModel.cs index ca1b757..9621963 100644 --- a/ConeCalorimeter/ViewModels/RealtimeDataRowViewModel.cs +++ b/ConeCalorimeter/ViewModels/RealtimeDataRowViewModel.cs @@ -17,7 +17,7 @@ public sealed class RealtimeDataRowViewModel CarbonMonoxideText = Format(record.CarbonMonoxide); OrificePressureText = Format(record.OrificePressure); OrificeTemperatureText = Format(record.OrificeTemperature); - AbsorbanceText = Format(record.Absorbance); + AbsorbanceText = Format(record.Absorbance, "0.00000"); Hrr50Text = Format(record.HeatReleaseRate); ThrText = Format(record.TotalHeatRelease); Spr50Text = Format(record.SmokeProduction); diff --git a/ConeCalorimeter/ViewModels/SmokeDensitySettingsViewModel.cs b/ConeCalorimeter/ViewModels/SmokeDensitySettingsViewModel.cs index 01e56a1..c14d1b8 100644 --- a/ConeCalorimeter/ViewModels/SmokeDensitySettingsViewModel.cs +++ b/ConeCalorimeter/ViewModels/SmokeDensitySettingsViewModel.cs @@ -5,6 +5,7 @@ using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ConeCalorimeter.Services; +using Serilog; namespace ConeCalorimeter.ViewModels; @@ -15,6 +16,15 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel private const double AbsorbanceMaximum = 100; private const double AbsorbanceStableTolerance = 0.25; private const int AbsorbanceStableReadCount = 2; + private const double AbsorbanceZeroTolerance = 0.000005; + private const string AbsorbanceFormat = "0.00000"; + private static readonly ModbusFloatByteOrder[] AbsorbanceByteOrders = + [ + ModbusFloatByteOrder.Abcd, + ModbusFloatByteOrder.Cdab, + ModbusFloatByteOrder.Badc, + ModbusFloatByteOrder.Dcba + ]; private readonly Action _closeAction; private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; @@ -25,6 +35,7 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel private double? _stableAbsorbance; private double? _pendingAbsorbance; private int _pendingAbsorbanceReadCount; + private ModbusFloatByteOrder? _absorbanceByteOrder; private string _absorbanceText = ""; private string _lastCalibration = "待校准"; @@ -125,12 +136,7 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel private void RefreshDeviceValues() { - if (!TryReadRangedFloatValue( - "Absorbance", - AbsorbanceRegister, - AbsorbanceMinimum, - AbsorbanceMaximum, - out var value)) + if (!TryReadAbsorbanceValue(out var value)) { if (!_stableAbsorbance.HasValue) { @@ -142,45 +148,71 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel if (TryUpdateStableAbsorbance(value, out var stableValue)) { - AbsorbanceText = stableValue.ToString("0.00", CultureInfo.InvariantCulture); + AbsorbanceText = FormatAbsorbance(stableValue); + } + else if (_stableAbsorbance.HasValue) + { + AbsorbanceText = FormatAbsorbance(_stableAbsorbance.Value); } } - private bool TryReadRangedFloatValue( - string label, - ushort registerAddress, - double minimum, - double maximum, - out double value) + private bool TryReadAbsorbanceValue(out double value) { value = double.NaN; - if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result)) + if (!_tcpDeviceConnectionService.TryReadFloatValues(AbsorbanceRegister, out var result)) { return false; } - if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var selectedValue, out var matchCount)) + var preferredByteOrder = _absorbanceByteOrder; + if (!ModbusFloatSelector.TrySelectRangedFloat( + result, + AbsorbanceMinimum, + AbsorbanceMaximum, + out var byteOrder, + out var selectedValue, + out var matchCount, + preferredByteOrder)) { - LogFloatDiagnostic(label, registerAddress, result, null, matchCount); - return false; + if (!TrySelectSingleNonZeroAbsorbanceCandidate(result, out byteOrder, out selectedValue)) + { + LogFloatDiagnostic(result, null, matchCount, preferredByteOrder); + return false; + } } - LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount); + if (!preferredByteOrder.HasValue + && IsZeroLikeAbsorbance(selectedValue) + && TrySelectSingleNonZeroAbsorbanceCandidate(result, out var nonZeroByteOrder, out var nonZeroValue)) + { + byteOrder = nonZeroByteOrder; + selectedValue = nonZeroValue; + } + + LogFloatDiagnostic(result, byteOrder, matchCount, preferredByteOrder); + RememberAbsorbanceByteOrder(byteOrder, selectedValue); value = NormalizeDisplayValue(selectedValue); return true; } private bool TryUpdateStableAbsorbance(double value, out double stableValue) { - stableValue = _stableAbsorbance ?? value; + value = NormalizeDisplayValue(value); - if (_stableAbsorbance.HasValue - && Math.Abs(value - _stableAbsorbance.Value) <= AbsorbanceStableTolerance) + if (!_stableAbsorbance.HasValue) { - _pendingAbsorbance = null; - _pendingAbsorbanceReadCount = 0; - stableValue = _stableAbsorbance.Value; + AcceptStableAbsorbance(value); + stableValue = value; + return true; + } + + stableValue = _stableAbsorbance.Value; + + if (Math.Abs(value - _stableAbsorbance.Value) <= AbsorbanceStableTolerance) + { + AcceptStableAbsorbance(value); + stableValue = value; return true; } @@ -199,44 +231,99 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel return false; } - _stableAbsorbance = value; - _pendingAbsorbance = null; - _pendingAbsorbanceReadCount = 0; + AcceptStableAbsorbance(value); stableValue = value; return true; } + private void AcceptStableAbsorbance(double value) + { + _stableAbsorbance = value; + _pendingAbsorbance = null; + _pendingAbsorbanceReadCount = 0; + } + + private static bool TrySelectSingleNonZeroAbsorbanceCandidate( + ModbusFloatReadResult result, + out ModbusFloatByteOrder byteOrder, + out double value) + { + var matches = AbsorbanceByteOrders + .Select(candidate => new + { + ByteOrder = candidate, + Value = result.GetValue(candidate) + }) + .Where(candidate => ModbusFloatSelector.IsInRange( + candidate.Value, + AbsorbanceMinimum, + AbsorbanceMaximum) + && !IsZeroLikeAbsorbance(candidate.Value)) + .ToArray(); + + if (matches.Length != 1) + { + byteOrder = default; + value = double.NaN; + return false; + } + + byteOrder = matches[0].ByteOrder; + value = matches[0].Value; + return true; + } + + private void RememberAbsorbanceByteOrder(ModbusFloatByteOrder byteOrder, double value) + { + if (!IsZeroLikeAbsorbance(value)) + { + _absorbanceByteOrder = byteOrder; + } + } + private void LogFloatDiagnostic( - string label, - ushort registerAddress, ModbusFloatReadResult result, ModbusFloatByteOrder? selectedByteOrder, - int matchCount) + int matchCount, + ModbusFloatByteOrder? preferredByteOrder) { if (selectedByteOrder is null) { - if (!_loggedInvalidFloatDiagnostics.Add(registerAddress)) + if (!_loggedInvalidFloatDiagnostics.Add(AbsorbanceRegister)) { return; } } - else if (!_loggedFloatDiagnostics.Add(registerAddress)) + else if (!_loggedFloatDiagnostics.Add(AbsorbanceRegister)) { return; } var selectedText = selectedByteOrder?.ToString() ?? "none"; - - Debug.WriteLine( - $"Smoke density float {label} register {registerAddress} raw [{result.RawHex}], " + var preferredText = preferredByteOrder?.ToString() ?? "none"; + var message = + $"Smoke density absorbance D{AbsorbanceRegister} raw [{result.RawHex}], " + $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, " + $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, " - + $"selected={selectedText}, matches={matchCount}."); + + $"selected={selectedText}, preferred={preferredText}, matches={matchCount}."; + + Debug.WriteLine(message); + Log.Debug(message); } private static double NormalizeDisplayValue(double value) { - return Math.Abs(value) < 0.005 ? 0 : value; + return IsZeroLikeAbsorbance(value) ? 0 : value; + } + + private static bool IsZeroLikeAbsorbance(double value) + { + return double.IsFinite(value) && Math.Abs(value) < AbsorbanceZeroTolerance; + } + + private static string FormatAbsorbance(double value) + { + return value.ToString(AbsorbanceFormat, CultureInfo.InvariantCulture); } private static bool TryGetCalibrationCoil(string label, out ushort coilAddress) diff --git a/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml b/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml index f4b7c3a..005727a 100644 --- a/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml +++ b/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml @@ -105,7 +105,7 @@ FontSize="22" Foreground="#111A18" />