更新
This commit is contained in:
50
ConeCalorimeter/Services/ModbusFloatSelector.cs
Normal file
50
ConeCalorimeter/Services/ModbusFloatSelector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user