更新
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 IgnitionSecondsRegister = 1014;
|
||||||
private const ushort TestSecondsRegister = 1015;
|
private const ushort TestSecondsRegister = 1015;
|
||||||
private const ushort M3FlameMonitorBit = 3;
|
private const ushort M3FlameMonitorBit = 3;
|
||||||
private static readonly ModbusFloatByteOrder[] AlternateFloatByteOrders =
|
|
||||||
[
|
|
||||||
ModbusFloatByteOrder.Cdab,
|
|
||||||
ModbusFloatByteOrder.Badc,
|
|
||||||
ModbusFloatByteOrder.Dcba
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||||
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
|
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
|
||||||
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
|
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
|
||||||
@@ -76,7 +69,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
|||||||
return double.NaN;
|
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);
|
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
|
||||||
return double.NaN;
|
return double.NaN;
|
||||||
@@ -86,40 +79,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
|||||||
return NormalizeRealtimeValue(value);
|
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(
|
private void LogFloatDiagnostic(
|
||||||
string label,
|
string label,
|
||||||
ushort registerAddress,
|
ushort registerAddress,
|
||||||
@@ -148,11 +107,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
|||||||
+ $"selected={selectedText}, matches={matchCount}.");
|
+ $"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)
|
private static double NormalizeRealtimeValue(double value)
|
||||||
{
|
{
|
||||||
return Math.Abs(value) < 0.005 ? 0 : 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 TargetTemperatureRegister = 400;
|
||||||
private const ushort CurrentHeatFluxRegister = 410;
|
private const ushort CurrentHeatFluxRegister = 410;
|
||||||
private const ushort HeatTransferRegister = 418;
|
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 AlarmCoil = 91;
|
||||||
private const ushort CirculatingWaterCoil = 49;
|
private const ushort CirculatingWaterCoil = 49;
|
||||||
private const ushort HeatingCoil = 102;
|
private const ushort HeatingCoil = 102;
|
||||||
@@ -21,6 +29,8 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
private readonly Action _helpAction;
|
private readonly Action _helpAction;
|
||||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||||
private readonly DispatcherTimer _refreshTimer;
|
private readonly DispatcherTimer _refreshTimer;
|
||||||
|
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
|
||||||
|
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
|
||||||
private string _currentTemperatureText = "";
|
private string _currentTemperatureText = "";
|
||||||
private string _currentHeatFluxText = "";
|
private string _currentHeatFluxText = "";
|
||||||
private string _targetTemperatureText = "";
|
private string _targetTemperatureText = "";
|
||||||
@@ -30,6 +40,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
private bool _circulatingWaterActive;
|
private bool _circulatingWaterActive;
|
||||||
private bool _heatingActive;
|
private bool _heatingActive;
|
||||||
private bool _isEditingConeParameters;
|
private bool _isEditingConeParameters;
|
||||||
|
private DateTime _parameterRefreshBlockedUntil = DateTime.MinValue;
|
||||||
|
|
||||||
public ConeRadiationSettingsViewModel(
|
public ConeRadiationSettingsViewModel(
|
||||||
Action closeAction,
|
Action closeAction,
|
||||||
@@ -133,6 +144,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
public void EndConeParameterEdit()
|
public void EndConeParameterEdit()
|
||||||
{
|
{
|
||||||
_isEditingConeParameters = false;
|
_isEditingConeParameters = false;
|
||||||
|
_parameterRefreshBlockedUntil = DateTime.UtcNow.AddMilliseconds(ParameterRefreshReleaseDelayMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteAction(string? action)
|
private void ExecuteAction(string? action)
|
||||||
@@ -152,8 +164,11 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
|
|
||||||
if (action == "开始升温")
|
if (action == "开始升温")
|
||||||
{
|
{
|
||||||
WriteTargetTemperature();
|
if (WriteTargetTemperature())
|
||||||
WriteActionCoil(action);
|
{
|
||||||
|
WriteActionCoil(action);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +177,37 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
|
|
||||||
private void RefreshDeviceValues()
|
private void RefreshDeviceValues()
|
||||||
{
|
{
|
||||||
CurrentTemperatureText = ReadFloatText(CurrentTemperatureRegister);
|
CurrentTemperatureText = TryReadRangedFloatText(
|
||||||
CurrentHeatFluxText = ReadFloatText(CurrentHeatFluxRegister);
|
"ConeCurrentTemperature",
|
||||||
if (!_isEditingConeParameters)
|
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;
|
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)
|
text = string.Empty;
|
||||||
? value.ToString("0.00", CultureInfo.InvariantCulture)
|
|
||||||
|
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;
|
: string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteTargetTemperature()
|
private bool WriteTargetTemperature()
|
||||||
{
|
{
|
||||||
if (!short.TryParse(TargetTemperatureText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
if (!short.TryParse(TargetTemperatureText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||||
{
|
{
|
||||||
LastAction = "辐射温度输入无效";
|
LastAction = "辐射温度输入无效";
|
||||||
Debug.WriteLine($"Invalid cone radiation target temperature: {TargetTemperatureText}");
|
Debug.WriteLine($"Invalid cone radiation target temperature: {TargetTemperatureText}");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, value))
|
if (_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, value))
|
||||||
{
|
{
|
||||||
LastAction = "设置辐射温度失败";
|
TargetTemperatureText = value.ToString(CultureInfo.InvariantCulture);
|
||||||
Debug.WriteLine("Cone radiation target temperature write failed.");
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LastAction = "设置辐射温度失败";
|
||||||
|
Debug.WriteLine("Cone radiation target temperature write failed.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveParameters()
|
private void SaveParameters()
|
||||||
@@ -209,8 +280,17 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ModbusFloatSelector.IsInRange(heatTransfer, HeatTransferInputMinimum, HeatTransferMaximum))
|
||||||
|
{
|
||||||
|
LastAction = "热传递输入超出范围";
|
||||||
|
Debug.WriteLine($"Cone radiation heat transfer out of range: {HeatTransferText}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_tcpDeviceConnectionService.TryWriteFloat(HeatTransferRegister, heatTransfer))
|
if (_tcpDeviceConnectionService.TryWriteFloat(HeatTransferRegister, heatTransfer))
|
||||||
{
|
{
|
||||||
|
_parameterRefreshBlockedUntil = DateTime.UtcNow.AddMilliseconds(ParameterRefreshReleaseDelayMilliseconds);
|
||||||
|
HeatTransferText = heatTransfer.ToString("0.##", CultureInfo.InvariantCulture);
|
||||||
LastAction = "热传递保存成功";
|
LastAction = "热传递保存成功";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -219,6 +299,44 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
Debug.WriteLine("Cone radiation heat transfer write failed.");
|
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()
|
private void ToggleCirculatingWater()
|
||||||
{
|
{
|
||||||
if (!_tcpDeviceConnectionService.TryReadCoil(CirculatingWaterCoil, out var isActive))
|
if (!_tcpDeviceConnectionService.TryReadCoil(CirculatingWaterCoil, out var isActive))
|
||||||
|
|||||||
@@ -217,7 +217,9 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBox Text="{Binding TargetTemperatureText, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding TargetTemperatureText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Style="{StaticResource ValueInputBoxStyle}" />
|
Style="{StaticResource ValueInputBoxStyle}"
|
||||||
|
GotKeyboardFocus="ConeParameterTextBox_GotKeyboardFocus"
|
||||||
|
LostKeyboardFocus="ConeParameterTextBox_LostKeyboardFocus" />
|
||||||
<TextBlock Grid.Column="3"
|
<TextBlock Grid.Column="3"
|
||||||
Text="°C"
|
Text="°C"
|
||||||
FontSize="24"
|
FontSize="24"
|
||||||
|
|||||||
Reference in New Issue
Block a user