This commit is contained in:
GukSang.Jin
2026-05-21 19:41:41 +08:00
parent 606c3c8187
commit 95a4d4d671
4 changed files with 186 additions and 62 deletions

View File

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

View File

@@ -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<ushort> _loggedFloatDiagnostics = [];
private readonly HashSet<ushort> _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;

View File

@@ -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<ushort> _loggedFloatDiagnostics = [];
private readonly HashSet<ushort> _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))

View File

@@ -217,7 +217,9 @@
</Grid.ColumnDefinitions>
<TextBox Text="{Binding TargetTemperatureText, UpdateSourceTrigger=PropertyChanged}"
Grid.Column="1"
Style="{StaticResource ValueInputBoxStyle}" />
Style="{StaticResource ValueInputBoxStyle}"
GotKeyboardFocus="ConeParameterTextBox_GotKeyboardFocus"
LostKeyboardFocus="ConeParameterTextBox_LostKeyboardFocus" />
<TextBlock Grid.Column="3"
Text="°C"
FontSize="24"