diff --git a/ConeCalorimeter/Services/ModbusFloatSelector.cs b/ConeCalorimeter/Services/ModbusFloatSelector.cs new file mode 100644 index 0000000..25fc2ae --- /dev/null +++ b/ConeCalorimeter/Services/ModbusFloatSelector.cs @@ -0,0 +1,50 @@ +namespace ConeCalorimeter.Services; + +internal static class ModbusFloatSelector +{ + private static readonly ModbusFloatByteOrder[] AlternateFloatByteOrders = + [ + ModbusFloatByteOrder.Cdab, + ModbusFloatByteOrder.Badc, + ModbusFloatByteOrder.Dcba + ]; + + public static bool TrySelectRangedFloat( + ModbusFloatReadResult result, + double minimum, + double maximum, + out ModbusFloatByteOrder byteOrder, + out double value, + out int matchCount) + { + var alternateMatches = AlternateFloatByteOrders + .Where(candidate => IsInRange(result.GetValue(candidate), minimum, maximum)) + .ToArray(); + var isAbcdInRange = IsInRange(result.Abcd, minimum, maximum); + + matchCount = alternateMatches.Length + (isAbcdInRange ? 1 : 0); + + if (isAbcdInRange) + { + byteOrder = ModbusFloatByteOrder.Abcd; + value = result.Abcd; + return true; + } + + if (alternateMatches.Length != 1) + { + byteOrder = default; + value = double.NaN; + return false; + } + + byteOrder = alternateMatches[0]; + value = result.GetValue(byteOrder); + return true; + } + + public static bool IsInRange(double value, double minimum, double maximum) + { + return double.IsFinite(value) && value >= minimum && value <= maximum; + } +} diff --git a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs index aa4c207..e35192d 100644 --- a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs +++ b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs @@ -24,13 +24,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService private const ushort IgnitionSecondsRegister = 1014; private const ushort TestSecondsRegister = 1015; private const ushort M3FlameMonitorBit = 3; - private static readonly ModbusFloatByteOrder[] AlternateFloatByteOrders = - [ - ModbusFloatByteOrder.Cdab, - ModbusFloatByteOrder.Badc, - ModbusFloatByteOrder.Dcba - ]; - private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; private readonly HashSet _loggedFloatDiagnostics = []; private readonly HashSet _loggedInvalidFloatDiagnostics = []; @@ -76,7 +69,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService return double.NaN; } - if (!TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount)) + if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount)) { LogFloatDiagnostic(label, registerAddress, result, null, matchCount); return double.NaN; @@ -86,40 +79,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService return NormalizeRealtimeValue(value); } - private static bool TrySelectRangedFloat( - ModbusFloatReadResult result, - double minimum, - double maximum, - out ModbusFloatByteOrder byteOrder, - out double value, - out int matchCount) - { - var alternateMatches = AlternateFloatByteOrders - .Where(candidate => IsInRange(result.GetValue(candidate), minimum, maximum)) - .ToArray(); - var isAbcdInRange = IsInRange(result.Abcd, minimum, maximum); - - matchCount = alternateMatches.Length + (isAbcdInRange ? 1 : 0); - - if (isAbcdInRange) - { - byteOrder = ModbusFloatByteOrder.Abcd; - value = result.Abcd; - return true; - } - - if (alternateMatches.Length != 1) - { - byteOrder = default; - value = double.NaN; - return false; - } - - byteOrder = alternateMatches[0]; - value = result.GetValue(byteOrder); - return true; - } - private void LogFloatDiagnostic( string label, ushort registerAddress, @@ -148,11 +107,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService + $"selected={selectedText}, matches={matchCount}."); } - private static bool IsInRange(double value, double minimum, double maximum) - { - return double.IsFinite(value) && value >= minimum && value <= maximum; - } - private static double NormalizeRealtimeValue(double value) { return Math.Abs(value) < 0.005 ? 0 : value; diff --git a/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs b/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs index b8305b6..0660416 100644 --- a/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs +++ b/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs @@ -12,6 +12,14 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel private const ushort TargetTemperatureRegister = 400; private const ushort CurrentHeatFluxRegister = 410; private const ushort HeatTransferRegister = 418; + private const double CurrentTemperatureMinimum = 0; + private const double CurrentTemperatureMaximum = 1200; + private const double CurrentHeatFluxMinimum = 0; + private const double CurrentHeatFluxMaximum = 1000; + private const double HeatTransferInputMinimum = 0; + private const double HeatTransferReadMinimum = 0.01; + private const double HeatTransferMaximum = 20000; + private const int ParameterRefreshReleaseDelayMilliseconds = 500; private const ushort AlarmCoil = 91; private const ushort CirculatingWaterCoil = 49; private const ushort HeatingCoil = 102; @@ -21,6 +29,8 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel private readonly Action _helpAction; private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; private readonly DispatcherTimer _refreshTimer; + private readonly HashSet _loggedFloatDiagnostics = []; + private readonly HashSet _loggedInvalidFloatDiagnostics = []; private string _currentTemperatureText = ""; private string _currentHeatFluxText = ""; private string _targetTemperatureText = ""; @@ -30,6 +40,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel private bool _circulatingWaterActive; private bool _heatingActive; private bool _isEditingConeParameters; + private DateTime _parameterRefreshBlockedUntil = DateTime.MinValue; public ConeRadiationSettingsViewModel( Action closeAction, @@ -133,6 +144,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel public void EndConeParameterEdit() { _isEditingConeParameters = false; + _parameterRefreshBlockedUntil = DateTime.UtcNow.AddMilliseconds(ParameterRefreshReleaseDelayMilliseconds); } private void ExecuteAction(string? action) @@ -152,8 +164,11 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel if (action == "开始升温") { - WriteTargetTemperature(); - WriteActionCoil(action); + if (WriteTargetTemperature()) + { + WriteActionCoil(action); + } + return; } @@ -162,11 +177,37 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel private void RefreshDeviceValues() { - CurrentTemperatureText = ReadFloatText(CurrentTemperatureRegister); - CurrentHeatFluxText = ReadFloatText(CurrentHeatFluxRegister); - if (!_isEditingConeParameters) + CurrentTemperatureText = TryReadRangedFloatText( + "ConeCurrentTemperature", + CurrentTemperatureRegister, + CurrentTemperatureMinimum, + CurrentTemperatureMaximum, + "0.0", + out var currentTemperatureText) + ? currentTemperatureText + : string.Empty; + CurrentHeatFluxText = TryReadRangedFloatText( + "ConeCurrentHeatFlux", + CurrentHeatFluxRegister, + CurrentHeatFluxMinimum, + CurrentHeatFluxMaximum, + "0.00", + out var currentHeatFluxText) + ? currentHeatFluxText + : string.Empty; + if (CanRefreshConeParameters()) { - HeatTransferText = ReadFloatText(HeatTransferRegister); + TargetTemperatureText = ReadInt16Text(TargetTemperatureRegister); + if (TryReadRangedFloatText( + "ConeHeatTransfer", + HeatTransferRegister, + HeatTransferReadMinimum, + HeatTransferMaximum, + "0.##", + out var heatTransferText)) + { + HeatTransferText = heatTransferText; + } } AlarmActive = _tcpDeviceConnectionService.TryReadCoil(AlarmCoil, out var alarmActive) && alarmActive; @@ -177,27 +218,57 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel } } - private string ReadFloatText(ushort registerAddress) + private bool TryReadRangedFloatText( + string label, + ushort registerAddress, + double minimum, + double maximum, + string format, + out string text) { - return _tcpDeviceConnectionService.TryReadFloat(registerAddress, out var value) - ? value.ToString("0.00", CultureInfo.InvariantCulture) + text = string.Empty; + + if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result)) + { + return false; + } + + if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount)) + { + LogFloatDiagnostic(label, registerAddress, result, null, matchCount); + return false; + } + + LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount); + text = NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture); + return true; + } + + private string ReadInt16Text(ushort registerAddress) + { + return _tcpDeviceConnectionService.TryReadInt16(registerAddress, out var value) + ? value.ToString(CultureInfo.InvariantCulture) : string.Empty; } - private void WriteTargetTemperature() + private bool WriteTargetTemperature() { if (!short.TryParse(TargetTemperatureText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { LastAction = "辐射温度输入无效"; Debug.WriteLine($"Invalid cone radiation target temperature: {TargetTemperatureText}"); - return; + return false; } - if (!_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, value)) + if (_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, value)) { - LastAction = "设置辐射温度失败"; - Debug.WriteLine("Cone radiation target temperature write failed."); + TargetTemperatureText = value.ToString(CultureInfo.InvariantCulture); + return true; } + + LastAction = "设置辐射温度失败"; + Debug.WriteLine("Cone radiation target temperature write failed."); + return false; } private void SaveParameters() @@ -209,8 +280,17 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel return; } + if (!ModbusFloatSelector.IsInRange(heatTransfer, HeatTransferInputMinimum, HeatTransferMaximum)) + { + LastAction = "热传递输入超出范围"; + Debug.WriteLine($"Cone radiation heat transfer out of range: {HeatTransferText}"); + return; + } + if (_tcpDeviceConnectionService.TryWriteFloat(HeatTransferRegister, heatTransfer)) { + _parameterRefreshBlockedUntil = DateTime.UtcNow.AddMilliseconds(ParameterRefreshReleaseDelayMilliseconds); + HeatTransferText = heatTransfer.ToString("0.##", CultureInfo.InvariantCulture); LastAction = "热传递保存成功"; return; } @@ -219,6 +299,44 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel Debug.WriteLine("Cone radiation heat transfer write failed."); } + private void LogFloatDiagnostic( + string label, + ushort registerAddress, + ModbusFloatReadResult result, + ModbusFloatByteOrder? selectedByteOrder, + int matchCount) + { + if (selectedByteOrder is null) + { + if (!_loggedInvalidFloatDiagnostics.Add(registerAddress)) + { + return; + } + } + else if (!_loggedFloatDiagnostics.Add(registerAddress)) + { + return; + } + + var selectedText = selectedByteOrder?.ToString() ?? "none"; + + Debug.WriteLine( + $"Cone radiation float {label} register {registerAddress} raw [{result.RawHex}], " + + $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, " + + $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, " + + $"selected={selectedText}, matches={matchCount}."); + } + + private static double NormalizeDisplayValue(double value) + { + return Math.Abs(value) < 0.005 ? 0 : value; + } + + private bool CanRefreshConeParameters() + { + return !_isEditingConeParameters && DateTime.UtcNow >= _parameterRefreshBlockedUntil; + } + private void ToggleCirculatingWater() { if (!_tcpDeviceConnectionService.TryReadCoil(CirculatingWaterCoil, out var isActive)) diff --git a/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml b/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml index 6dfa538..c998d27 100644 --- a/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml +++ b/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml @@ -217,7 +217,9 @@ + Style="{StaticResource ValueInputBoxStyle}" + GotKeyboardFocus="ConeParameterTextBox_GotKeyboardFocus" + LostKeyboardFocus="ConeParameterTextBox_LostKeyboardFocus" />