This commit is contained in:
GukSang.Jin
2026-05-28 16:23:57 +08:00
parent a700cab0b2
commit 51d4010d29
4 changed files with 136 additions and 82 deletions

View File

@@ -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<ushort> _loggedInvalidFloatDiagnostics = [];
private readonly Dictionary<int, double> _stableRealtimeValues = [];
private readonly Dictionary<int, int> _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,

View File

@@ -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);

View File

@@ -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)

View File

@@ -105,7 +105,7 @@
FontSize="22"
Foreground="#111A18" />
<TextBlock Margin="0,14,0,0"
Text="2. 25%为放入75%遮光片校准;"
Text="2. 25%为放入25%遮光片校准;"
FontSize="22"
Foreground="#111A18" />
<TextBlock Margin="0,14,0,0"
@@ -113,7 +113,7 @@
FontSize="22"
Foreground="#111A18" />
<TextBlock Margin="0,14,0,0"
Text="4. 75%为放入25%遮光片校准;"
Text="4. 75%为放入75%遮光片校准;"
FontSize="22"
Foreground="#111A18" />
<TextBlock Margin="0,14,0,0"