Files
ConeCalorimeter/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs
GukSang.Jin 5477fdf210 更新
2026-05-26 20:12:08 +08:00

600 lines
20 KiB
C#

using System.Diagnostics;
using System.Globalization;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.Input;
using ConeCalorimeter.Models;
using ConeCalorimeter.Services;
namespace ConeCalorimeter.ViewModels;
public sealed class ConeRadiationSettingsViewModel : PageViewModel
{
private const ushort CurrentHeatFluxRegister = 32;
private const ushort TargetTemperatureRegister = 400;
private const ushort HeatTransferRegister = 418;
private const double TargetTemperatureScale = 10;
private const double CurrentTemperatureMinimum = 10;
private const double CurrentTemperatureMaximum = 1200;
private const double CurrentTemperatureMaximumDropPerRefresh = 80;
private const double CurrentHeatFluxMinimum = 0;
private const double CurrentHeatFluxMaximum = 1000;
private const double CurrentHeatFluxZeroTolerance = 0.005;
private const double CurrentHeatFluxStableAbsoluteTolerance = 0.25;
private const double CurrentHeatFluxStableRelativeTolerance = 0.1;
private const double CurrentHeatFluxStableMaximumTolerance = 5;
private const int CurrentHeatFluxStableReadCount = 3;
private const int CurrentHeatFluxZeroStableReadCount = 10;
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;
private const string CirculatingWaterAction = "循环水";
private readonly Action _closeAction;
private readonly Action _helpAction;
private readonly IExperimentDataService _experimentDataService;
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
private readonly DispatcherTimer _refreshTimer;
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
private static readonly ModbusFloatByteOrder[] CurrentHeatFluxByteOrders =
[
ModbusFloatByteOrder.Abcd,
ModbusFloatByteOrder.Cdab,
ModbusFloatByteOrder.Badc,
ModbusFloatByteOrder.Dcba
];
private string _currentTemperatureText = "";
private string _currentHeatFluxText = "";
private string _targetTemperatureText = "";
private string _heatTransferText = "";
private string _lastAction = "待机";
private bool _alarmActive;
private bool _circulatingWaterActive;
private bool _heatingActive;
private bool _isEditingConeParameters;
private double? _lastStableCurrentTemperature;
private double? _lastStableCurrentHeatFlux;
private double? _pendingCurrentHeatFlux;
private int _pendingCurrentHeatFluxReadCount;
private int _pendingCurrentHeatFluxZeroReadCount;
private ModbusFloatByteOrder? _currentHeatFluxByteOrder;
private DateTime _parameterRefreshBlockedUntil = DateTime.MinValue;
public ConeRadiationSettingsViewModel(
Action closeAction,
Action helpAction,
IExperimentDataService experimentDataService,
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("辐射锥设置")
{
_closeAction = closeAction;
_helpAction = helpAction;
_experimentDataService = experimentDataService;
_tcpDeviceConnectionService = tcpDeviceConnectionService;
CloseCommand = new RelayCommand(_closeAction);
HelpCommand = new RelayCommand(_helpAction);
ActionCommand = new RelayCommand<string>(ExecuteAction);
SaveParametersCommand = new RelayCommand(SaveParameters);
UpdateCurrentReadings(_experimentDataService.CurrentSnapshot);
_experimentDataService.SnapshotUpdated += (_, snapshot) => UpdateCurrentReadings(snapshot);
RefreshDeviceValues();
_refreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_refreshTimer.Tick += (_, _) => RefreshDeviceValues();
_refreshTimer.Start();
}
public IRelayCommand CloseCommand { get; }
public IRelayCommand HelpCommand { get; }
public IRelayCommand<string> ActionCommand { get; }
public IRelayCommand SaveParametersCommand { get; }
public string CurrentTemperatureText
{
get => _currentTemperatureText;
private set => SetProperty(ref _currentTemperatureText, value);
}
public string CurrentHeatFluxText
{
get => _currentHeatFluxText;
private set => SetProperty(ref _currentHeatFluxText, value);
}
public string TargetTemperatureText
{
get => _targetTemperatureText;
set => SetProperty(ref _targetTemperatureText, value);
}
public string HeatTransferText
{
get => _heatTransferText;
set => SetProperty(ref _heatTransferText, value);
}
public string LastAction
{
get => _lastAction;
private set => SetProperty(ref _lastAction, value);
}
public bool AlarmActive
{
get => _alarmActive;
private set => SetProperty(ref _alarmActive, value);
}
public bool CirculatingWaterActive
{
get => _circulatingWaterActive;
private set
{
if (SetProperty(ref _circulatingWaterActive, value))
{
OnPropertyChanged(nameof(CirculatingWaterButtonText));
}
}
}
public string CirculatingWaterButtonText => CirculatingWaterActive ? "循环水:开启" : "循环水:关闭";
public bool HeatingActive
{
get => _heatingActive;
private set
{
if (SetProperty(ref _heatingActive, value))
{
OnPropertyChanged(nameof(StartHeatingButtonText));
}
}
}
public string StartHeatingButtonText => HeatingActive ? "加热中" : "开始升温";
public void BeginConeParameterEdit()
{
_isEditingConeParameters = true;
}
public void EndConeParameterEdit()
{
_isEditingConeParameters = false;
_parameterRefreshBlockedUntil = DateTime.UtcNow.AddMilliseconds(ParameterRefreshReleaseDelayMilliseconds);
}
private void ExecuteAction(string? action)
{
if (string.IsNullOrWhiteSpace(action))
{
return;
}
LastAction = action;
if (action == CirculatingWaterAction)
{
ToggleCirculatingWater();
return;
}
if (action == "开始升温")
{
if (WriteTargetTemperature())
{
WriteActionCoil(action);
}
return;
}
WriteActionCoil(action);
}
private void RefreshDeviceValues()
{
if (TryReadCurrentHeatFlux(out var currentHeatFlux))
{
if (TryUpdateStableCurrentHeatFlux(currentHeatFlux, out var stableCurrentHeatFlux))
{
CurrentHeatFluxText = stableCurrentHeatFlux.ToString("0.00", CultureInfo.InvariantCulture);
}
}
else if (!_lastStableCurrentHeatFlux.HasValue)
{
CurrentHeatFluxText = string.Empty;
}
if (CanRefreshConeParameters())
{
TargetTemperatureText = ReadScaledInt16Text(TargetTemperatureRegister, TargetTemperatureScale, "0.#");
if (TryReadRangedFloatText(
"ConeHeatTransfer",
HeatTransferRegister,
HeatTransferReadMinimum,
HeatTransferMaximum,
"0.##",
out var heatTransferText,
out _))
{
HeatTransferText = heatTransferText;
}
}
AlarmActive = _tcpDeviceConnectionService.TryReadCoil(AlarmCoil, out var alarmActive) && alarmActive;
HeatingActive = _tcpDeviceConnectionService.TryReadCoil(HeatingCoil, out var heatingActive) && heatingActive;
if (_tcpDeviceConnectionService.TryReadCoil(CirculatingWaterCoil, out var circulatingWaterActive))
{
CirculatingWaterActive = circulatingWaterActive;
}
}
private void UpdateCurrentReadings(RealtimeSnapshot snapshot)
{
if (IsStableCurrentTemperature(snapshot.ConeTemperature))
{
_lastStableCurrentTemperature = snapshot.ConeTemperature;
CurrentTemperatureText = NormalizeDisplayValue(snapshot.ConeTemperature)
.ToString("0.0", CultureInfo.InvariantCulture);
}
}
private bool TryReadCurrentHeatFlux(out double value)
{
value = double.NaN;
if (!_tcpDeviceConnectionService.TryReadFloatValues(CurrentHeatFluxRegister, out var result))
{
return false;
}
var preferredByteOrder = _currentHeatFluxByteOrder;
if (!ModbusFloatSelector.TrySelectRangedFloat(
result,
CurrentHeatFluxMinimum,
CurrentHeatFluxMaximum,
out var byteOrder,
out var selectedValue,
out var matchCount,
preferredByteOrder))
{
if (!TrySelectSingleNonZeroCurrentHeatFluxCandidate(result, out byteOrder, out selectedValue))
{
LogFloatDiagnostic("ConeCurrentHeatFlux", CurrentHeatFluxRegister, result, null, matchCount);
return false;
}
}
if (!preferredByteOrder.HasValue
&& IsZeroLikeCurrentHeatFlux(selectedValue)
&& TrySelectSingleNonZeroCurrentHeatFluxCandidate(result, out var nonZeroByteOrder, out var nonZeroValue))
{
byteOrder = nonZeroByteOrder;
selectedValue = nonZeroValue;
}
LogFloatDiagnostic("ConeCurrentHeatFlux", CurrentHeatFluxRegister, result, byteOrder, matchCount);
RememberCurrentHeatFluxByteOrder(byteOrder, selectedValue);
value = NormalizeDisplayValue(selectedValue);
return true;
}
private bool TryReadRangedFloatText(
string label,
ushort registerAddress,
double minimum,
double maximum,
string format,
out string text,
out double value)
{
text = string.Empty;
value = double.NaN;
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
{
return false;
}
if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var selectedValue, out var matchCount))
{
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
return false;
}
value = selectedValue;
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
text = NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture);
return true;
}
private bool IsStableCurrentTemperature(double value)
{
if (!ModbusFloatSelector.IsInRange(value, CurrentTemperatureMinimum, CurrentTemperatureMaximum))
{
return false;
}
return !_lastStableCurrentTemperature.HasValue
|| value >= _lastStableCurrentTemperature.Value - CurrentTemperatureMaximumDropPerRefresh;
}
private bool TryUpdateStableCurrentHeatFlux(double value, out double stableValue)
{
value = NormalizeDisplayValue(value);
stableValue = _lastStableCurrentHeatFlux ?? value;
if (IsZeroLikeCurrentHeatFlux(value))
{
_pendingCurrentHeatFlux = null;
_pendingCurrentHeatFluxReadCount = 0;
_pendingCurrentHeatFluxZeroReadCount++;
if (_pendingCurrentHeatFluxZeroReadCount < CurrentHeatFluxZeroStableReadCount)
{
return false;
}
AcceptStableCurrentHeatFlux(0);
stableValue = 0;
return true;
}
_pendingCurrentHeatFluxZeroReadCount = 0;
if (_lastStableCurrentHeatFlux.HasValue
&& IsWithinCurrentHeatFluxStableTolerance(value, _lastStableCurrentHeatFlux.Value))
{
AcceptStableCurrentHeatFlux(value);
stableValue = value;
return true;
}
if (!_pendingCurrentHeatFlux.HasValue
|| !IsWithinCurrentHeatFluxStableTolerance(value, _pendingCurrentHeatFlux.Value))
{
_pendingCurrentHeatFlux = value;
_pendingCurrentHeatFluxReadCount = 1;
return false;
}
_pendingCurrentHeatFluxReadCount++;
if (_pendingCurrentHeatFluxReadCount < CurrentHeatFluxStableReadCount)
{
return false;
}
AcceptStableCurrentHeatFlux(value);
stableValue = value;
return true;
}
private void AcceptStableCurrentHeatFlux(double value)
{
_lastStableCurrentHeatFlux = value;
_pendingCurrentHeatFlux = null;
_pendingCurrentHeatFluxReadCount = 0;
_pendingCurrentHeatFluxZeroReadCount = 0;
}
private static bool TrySelectSingleNonZeroCurrentHeatFluxCandidate(
ModbusFloatReadResult result,
out ModbusFloatByteOrder byteOrder,
out double value)
{
var matches = CurrentHeatFluxByteOrders
.Select(candidate => new
{
ByteOrder = candidate,
Value = result.GetValue(candidate)
})
.Where(candidate => ModbusFloatSelector.IsInRange(
candidate.Value,
CurrentHeatFluxMinimum,
CurrentHeatFluxMaximum)
&& !IsZeroLikeCurrentHeatFlux(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 RememberCurrentHeatFluxByteOrder(ModbusFloatByteOrder byteOrder, double value)
{
if (!IsZeroLikeCurrentHeatFlux(value))
{
_currentHeatFluxByteOrder = byteOrder;
}
}
private static bool IsWithinCurrentHeatFluxStableTolerance(double value, double referenceValue)
{
return Math.Abs(value - referenceValue) <= GetCurrentHeatFluxStableTolerance(referenceValue);
}
private static double GetCurrentHeatFluxStableTolerance(double referenceValue)
{
return Math.Min(
CurrentHeatFluxStableMaximumTolerance,
Math.Max(CurrentHeatFluxStableAbsoluteTolerance, Math.Abs(referenceValue) * CurrentHeatFluxStableRelativeTolerance));
}
private static bool IsZeroLikeCurrentHeatFlux(double value)
{
return double.IsFinite(value) && Math.Abs(value) < CurrentHeatFluxZeroTolerance;
}
private string ReadScaledInt16Text(ushort registerAddress, double scale, string format)
{
return _tcpDeviceConnectionService.TryReadInt16(registerAddress, out var rawValue)
? (rawValue / scale).ToString(format, CultureInfo.InvariantCulture)
: string.Empty;
}
private bool WriteTargetTemperature()
{
if (!double.TryParse(TargetTemperatureText, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
|| !double.IsFinite(value))
{
LastAction = "辐射温度输入无效";
Debug.WriteLine($"Invalid cone radiation target temperature: {TargetTemperatureText}");
return false;
}
var scaledValue = Math.Round(value * TargetTemperatureScale, MidpointRounding.AwayFromZero);
if (scaledValue is < short.MinValue or > short.MaxValue)
{
LastAction = "辐射温度输入超出范围";
Debug.WriteLine($"Cone radiation target temperature out of range: {TargetTemperatureText}");
return false;
}
if (_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, (short)scaledValue))
{
TargetTemperatureText = value.ToString("0.#", CultureInfo.InvariantCulture);
return true;
}
LastAction = "设置辐射温度失败";
Debug.WriteLine("Cone radiation target temperature write failed.");
return false;
}
private void SaveParameters()
{
if (!float.TryParse(HeatTransferText, NumberStyles.Float, CultureInfo.InvariantCulture, out var heatTransfer))
{
LastAction = "热传递输入无效";
Debug.WriteLine($"Invalid cone radiation heat transfer: {HeatTransferText}");
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;
}
LastAction = "热传递保存失败";
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))
{
LastAction = "循环水状态读取失败";
Debug.WriteLine("Cone radiation circulating water state read failed.");
return;
}
var nextValue = !isActive;
if (_tcpDeviceConnectionService.TryWriteCoil(CirculatingWaterCoil, nextValue))
{
CirculatingWaterActive = nextValue;
LastAction = $"循环水{(nextValue ? "" : "")}";
return;
}
LastAction = $"循环水{(nextValue ? "" : "")}失败";
Debug.WriteLine("Cone radiation circulating water write failed.");
}
private void WriteActionCoil(string action)
{
if (!TryGetActionCoil(action, out var coilAddress))
{
return;
}
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
{
LastAction = $"{action}失败";
Debug.WriteLine($"Cone radiation action '{action}' write failed.");
}
}
private static bool TryGetActionCoil(string action, out ushort coilAddress)
{
switch (action)
{
case "开始升温":
coilAddress = 100;
return true;
case "停止升温":
coilAddress = 101;
return true;
default:
coilAddress = 0;
return false;
}
}
}