Files
ConeCalorimeter/ConeCalorimeter/ViewModels/CValueCalibrationViewModel.cs
2026-05-25 18:54:22 +08:00

676 lines
22 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ConeCalorimeter.Services;
namespace ConeCalorimeter.ViewModels;
public sealed class CValueCalibrationViewModel : PageViewModel
{
private const ushort BaselineOxygenRegister = 268;
private const ushort TemperatureRegister = 282;
private const ushort PressureDifferenceRegister = 284;
private const ushort CalibrationOxygenRegister = 286;
private const ushort CValueRegister = 308;
private const double OxygenMinimum = 0;
private const double OxygenMaximum = 30;
private const double TemperatureMinimum = 200;
private const double TemperatureMaximum = 1500;
private const double PressureDifferenceMinimum = -5000;
private const double PressureDifferenceMaximum = 5000;
private const double CValueMinimum = 0;
private const double CValueMaximum = 1000;
private const double FractionalOxygenMaximum = 0.3;
private const double PlausibleOxygenMinimum = 1;
private const ushort CirculatingWaterCoil = 49;
private const ushort SamplingPumpCoil = 50;
private const ushort IgniterCoil = 53;
private const ushort FanCoil = 54;
private const ushort MethaneValveCoil = 55;
private const ushort BaselineCollectionCoil = 60;
private const ushort BaselineEndCoil = 62;
private const ushort CalibrationStartCoil = 70;
private const ushort CalibrationEndCoil = 72;
private static readonly TimeSpan PairedActionPulseDuration = TimeSpan.FromMilliseconds(300);
private const string CirculatingWaterAction = "循环水";
private const string SamplingPumpAction = "取样泵";
private const string IgniterAction = "点火器";
private const string FanAction = "风机";
private const string MethaneValveAction = "甲烷阀";
private const string BaselineCollectionAction = "基线采集";
private const string CalibrationStartAction = "标定开始";
private static readonly ModbusFloatByteOrder[] FloatByteOrders =
[
ModbusFloatByteOrder.Abcd,
ModbusFloatByteOrder.Cdab,
ModbusFloatByteOrder.Badc,
ModbusFloatByteOrder.Dcba
];
private readonly Action _closeAction;
private readonly Action _helpAction;
private readonly Action _baselineCollectionCompletedAction;
private readonly Action _calibrationCompletedAction;
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
private readonly DispatcherTimer _refreshTimer;
private readonly DispatcherTimer _pairedActionCompletionTimer;
private readonly Dictionary<string, CValueCalibrationActionViewModel> _actionsByLabel = [];
private readonly Dictionary<string, PairedActionWaitState> _pairedActionWaitStates = [];
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
private string _baselineOxygenText = "";
private string _temperatureText = "";
private string _pressureDifferenceText = "";
private string _calibrationOxygenText = "";
private string _cValueText = "";
private string _lastAction = "待机";
public CValueCalibrationViewModel(
Action closeAction,
Action helpAction,
Action baselineCollectionCompletedAction,
Action calibrationCompletedAction,
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("C值标定")
{
_closeAction = closeAction;
_helpAction = helpAction;
_baselineCollectionCompletedAction = baselineCollectionCompletedAction;
_calibrationCompletedAction = calibrationCompletedAction;
_tcpDeviceConnectionService = tcpDeviceConnectionService;
CloseCommand = new RelayCommand(_closeAction);
HelpCommand = new RelayCommand(_helpAction);
ActionCommand = new RelayCommand<string>(ExecuteAction);
TopActions =
[
CreateAction(CirculatingWaterAction, $"{CirculatingWaterAction}:关闭", $"{CirculatingWaterAction}:开启"),
CreateAction(MethaneValveAction, $"{MethaneValveAction}:关闭", $"{MethaneValveAction}:开启"),
CreateAction(FanAction, $"{FanAction}:关闭", $"{FanAction}:开启"),
CreateAction(IgniterAction, $"{IgniterAction}:关闭", $"{IgniterAction}:开启"),
CreateAction(SamplingPumpAction, $"{SamplingPumpAction}:关闭", $"{SamplingPumpAction}:开启")
];
BottomActions =
[
CreateAction(CalibrationStartAction, CalibrationStartAction, "标定结束"),
CreateAction(BaselineCollectionAction, BaselineCollectionAction, "基线结束")
];
_actionsByLabel[CalibrationStartAction].UpdateStatus(false);
_actionsByLabel[BaselineCollectionAction].UpdateStatus(false);
_pairedActionWaitStates[CalibrationStartAction] = new PairedActionWaitState(
CalibrationEndCoil,
"标定中",
"标定完成",
_calibrationCompletedAction);
_pairedActionWaitStates[BaselineCollectionAction] = new PairedActionWaitState(
BaselineEndCoil,
"基线采集中",
"基线采集完成",
_baselineCollectionCompletedAction);
_pairedActionCompletionTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(250)
};
_pairedActionCompletionTimer.Tick += (_, _) => RefreshPairedActionCompletions();
RefreshDeviceValues();
_refreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_refreshTimer.Tick += (_, _) => RefreshDeviceValues();
_refreshTimer.Start();
}
public ObservableCollection<CValueCalibrationActionViewModel> TopActions { get; }
public ObservableCollection<CValueCalibrationActionViewModel> BottomActions { get; }
public IRelayCommand CloseCommand { get; }
public IRelayCommand HelpCommand { get; }
public IRelayCommand<string> ActionCommand { get; }
public string BaselineOxygenText
{
get => _baselineOxygenText;
private set => SetProperty(ref _baselineOxygenText, value);
}
public string TemperatureText
{
get => _temperatureText;
private set => SetProperty(ref _temperatureText, value);
}
public string PressureDifferenceText
{
get => _pressureDifferenceText;
private set => SetProperty(ref _pressureDifferenceText, value);
}
public string HeatInputText { get; } = "5.0";
public string CalibrationOxygenText
{
get => _calibrationOxygenText;
private set => SetProperty(ref _calibrationOxygenText, value);
}
public string CValueText
{
get => _cValueText;
private set => SetProperty(ref _cValueText, value);
}
public string LastAction
{
get => _lastAction;
private set => SetProperty(ref _lastAction, value);
}
private void ExecuteAction(string? action)
{
if (string.IsNullOrWhiteSpace(action))
{
return;
}
if (IsPairedAction(action))
{
StartPairedAction(action);
return;
}
LastAction = action;
ToggleHoldAction(action);
}
private void RefreshDeviceValues()
{
BaselineOxygenText = ReadOxygenPercentText(
"BaselineOxygen",
BaselineOxygenRegister,
"0.00");
TemperatureText = ReadRangedFloatText(
"Temperature",
TemperatureRegister,
TemperatureMinimum,
TemperatureMaximum,
"0.00");
PressureDifferenceText = ReadRangedFloatText(
"PressureDifference",
PressureDifferenceRegister,
PressureDifferenceMinimum,
PressureDifferenceMaximum,
"0.00");
CalibrationOxygenText = ReadRangedFloatText(
"CalibrationOxygen",
CalibrationOxygenRegister,
OxygenMinimum,
OxygenMaximum,
"0.00");
CValueText = ReadRangedFloatText(
"CValue",
CValueRegister,
CValueMinimum,
CValueMaximum,
"0.00");
RefreshHoldActionStates();
}
private string ReadRangedFloatText(
string label,
ushort registerAddress,
double minimum,
double maximum,
string format)
{
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
{
return string.Empty;
}
if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount))
{
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
return string.Empty;
}
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
return NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture);
}
private string ReadOxygenPercentText(
string label,
ushort registerAddress,
string format)
{
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
{
return string.Empty;
}
if (!TrySelectOxygenPercent(result, out var byteOrder, out var value, out var matchCount))
{
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
return string.Empty;
}
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
return NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture);
}
private static bool TrySelectOxygenPercent(
ModbusFloatReadResult result,
out ModbusFloatByteOrder byteOrder,
out double value,
out int matchCount)
{
var candidates = FloatByteOrders
.Select(candidate => CreateOxygenCandidate(candidate, result.GetValue(candidate)))
.Where(candidate => candidate.IsValid)
.ToArray();
matchCount = candidates.Length;
var selected = candidates.FirstOrDefault(
candidate => candidate.ByteOrder == ModbusFloatByteOrder.Abcd && candidate.IsPlausible);
if (!selected.IsValid)
{
var plausibleCandidates = candidates
.Where(candidate => candidate.IsPlausible)
.ToArray();
if (plausibleCandidates.Length == 1)
{
selected = plausibleCandidates[0];
}
}
if (!selected.IsValid)
{
selected = candidates.FirstOrDefault(candidate => candidate.ByteOrder == ModbusFloatByteOrder.Abcd);
}
if (!selected.IsValid && candidates.Length == 1)
{
selected = candidates[0];
}
if (!selected.IsValid)
{
byteOrder = default;
value = double.NaN;
return false;
}
byteOrder = selected.ByteOrder;
value = selected.PercentValue;
return true;
}
private static OxygenPercentCandidate CreateOxygenCandidate(
ModbusFloatByteOrder byteOrder,
double rawValue)
{
if (!double.IsFinite(rawValue) || rawValue < OxygenMinimum)
{
return default;
}
double percentValue;
if (rawValue > OxygenMinimum && rawValue <= FractionalOxygenMaximum)
{
percentValue = rawValue * 100;
}
else if (rawValue <= OxygenMaximum)
{
percentValue = rawValue;
}
else
{
return default;
}
return new OxygenPercentCandidate(
byteOrder,
percentValue,
percentValue >= PlausibleOxygenMinimum && percentValue <= OxygenMaximum,
true);
}
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(
$"C value calibration 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 CValueCalibrationActionViewModel CreateAction(
string label,
string inactiveDisplayText,
string activeDisplayText)
{
var action = new CValueCalibrationActionViewModel(label, ActionCommand, inactiveDisplayText, activeDisplayText);
_actionsByLabel[label] = action;
return action;
}
private void RefreshHoldActionStates()
{
UpdateActionStatus(CirculatingWaterAction, CirculatingWaterCoil);
UpdateActionStatus(MethaneValveAction, MethaneValveCoil);
UpdateActionStatus(FanAction, FanCoil);
UpdateActionStatus(IgniterAction, IgniterCoil);
UpdateActionStatus(SamplingPumpAction, SamplingPumpCoil);
}
private bool UpdateActionStatus(string action, ushort coilAddress)
{
if (!_actionsByLabel.TryGetValue(action, out var actionViewModel))
{
return false;
}
if (_tcpDeviceConnectionService.TryReadCoil(coilAddress, out var isActive))
{
actionViewModel.UpdateStatus(isActive);
return true;
}
actionViewModel.UpdateStatus(actionViewModel.IsActive, false);
return false;
}
private void ToggleHoldAction(string action)
{
if (!TryGetHoldActionCoil(action, out var coilAddress))
{
return;
}
if (!_tcpDeviceConnectionService.TryReadCoil(coilAddress, out var isActive))
{
LastAction = $"{action}状态读取失败";
Debug.WriteLine($"C value calibration action '{action}' state read failed.");
return;
}
var nextValue = !isActive;
if (_tcpDeviceConnectionService.TryWriteCoil(coilAddress, nextValue))
{
LastAction = $"{action}{(nextValue ? "" : "")}";
if (!UpdateActionStatus(action, coilAddress) && _actionsByLabel.TryGetValue(action, out var actionViewModel))
{
actionViewModel.UpdateStatus(nextValue);
}
return;
}
LastAction = $"{action}{(nextValue ? "" : "")}失败";
Debug.WriteLine($"C value calibration action '{action}' write failed.");
}
private void StartPairedAction(string action)
{
if (!TryGetPairedActionCoils(action, out var startCoilAddress, out var endCoilAddress, out var endActionText))
{
return;
}
if (!_actionsByLabel.TryGetValue(action, out var actionViewModel))
{
return;
}
if (actionViewModel.IsBusy)
{
return;
}
if (!_pairedActionWaitStates.TryGetValue(action, out var waitState))
{
return;
}
if (!TryStartCoilPulse(startCoilAddress, action))
{
LastAction = $"{action}失败";
Debug.WriteLine($"C value calibration action '{action}' write failed.");
return;
}
var completionIsAlreadyActive = _tcpDeviceConnectionService.TryReadCoil(endCoilAddress, out var isComplete) && isComplete;
waitState.Begin(completionIsAlreadyActive);
actionViewModel.BeginWaiting(waitState.WaitingText);
_pairedActionCompletionTimer.Start();
LastAction = waitState.WaitingText;
if (completionIsAlreadyActive)
{
Debug.WriteLine(
$"C value calibration completion coil {endCoilAddress} was already active when '{action}' started; "
+ "waiting for it to reset before accepting completion.");
}
}
private void RefreshPairedActionCompletions()
{
var hasWaitingAction = false;
foreach (var (action, waitState) in _pairedActionWaitStates)
{
if (!waitState.IsWaiting)
{
continue;
}
hasWaitingAction = true;
if (!_actionsByLabel.TryGetValue(action, out var actionViewModel))
{
continue;
}
if (!_tcpDeviceConnectionService.TryReadCoil(waitState.CompletionCoilAddress, out var isComplete))
{
LastAction = $"{waitState.WaitingText},完成状态读取失败";
Debug.WriteLine(
$"C value calibration completion coil {waitState.CompletionCoilAddress} read failed for '{action}'.");
continue;
}
if (!isComplete)
{
waitState.MarkCompletionInactive();
continue;
}
if (waitState.RequiresInactiveBeforeCompletion && !waitState.HasSeenInactiveCompletionState)
{
continue;
}
CompletePairedAction(waitState, actionViewModel);
}
if (!hasWaitingAction)
{
_pairedActionCompletionTimer.Stop();
}
}
private void CompletePairedAction(
PairedActionWaitState waitState,
CValueCalibrationActionViewModel actionViewModel)
{
LastAction = waitState.CompletedText;
waitState.End();
try
{
waitState.CompletedAction();
}
finally
{
actionViewModel.FinishWaiting();
}
}
private bool TryStartCoilPulse(ushort coilAddress, string actionText)
{
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
{
return false;
}
_ = ReleaseCoilAfterPulseAsync(coilAddress, actionText);
return true;
}
private async Task ReleaseCoilAfterPulseAsync(ushort coilAddress, string actionText)
{
await Task.Delay(PairedActionPulseDuration);
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false))
{
Debug.WriteLine($"C value calibration action '{actionText}' pulse release failed.");
}
}
private static bool TryGetHoldActionCoil(string action, out ushort coilAddress)
{
switch (action)
{
case CirculatingWaterAction:
coilAddress = CirculatingWaterCoil;
return true;
case MethaneValveAction:
coilAddress = MethaneValveCoil;
return true;
case FanAction:
coilAddress = FanCoil;
return true;
case IgniterAction:
coilAddress = IgniterCoil;
return true;
case SamplingPumpAction:
coilAddress = SamplingPumpCoil;
return true;
default:
coilAddress = 0;
return false;
}
}
private static bool TryGetPairedActionCoils(
string action,
out ushort startCoilAddress,
out ushort endCoilAddress,
out string endActionText)
{
switch (action)
{
case BaselineCollectionAction:
startCoilAddress = BaselineCollectionCoil;
endCoilAddress = BaselineEndCoil;
endActionText = "基线结束";
return true;
case CalibrationStartAction:
startCoilAddress = CalibrationStartCoil;
endCoilAddress = CalibrationEndCoil;
endActionText = "标定结束";
return true;
default:
startCoilAddress = 0;
endCoilAddress = 0;
endActionText = string.Empty;
return false;
}
}
private static bool IsPairedAction(string action)
{
return action is BaselineCollectionAction or CalibrationStartAction;
}
private readonly record struct OxygenPercentCandidate(
ModbusFloatByteOrder ByteOrder,
double PercentValue,
bool IsPlausible,
bool IsValid);
private sealed class PairedActionWaitState(
ushort completionCoilAddress,
string waitingText,
string completedText,
Action completedAction)
{
public ushort CompletionCoilAddress { get; } = completionCoilAddress;
public string WaitingText { get; } = waitingText;
public string CompletedText { get; } = completedText;
public Action CompletedAction { get; } = completedAction;
public bool IsWaiting { get; private set; }
public bool RequiresInactiveBeforeCompletion { get; private set; }
public bool HasSeenInactiveCompletionState { get; private set; }
public void Begin(bool completionIsAlreadyActive)
{
IsWaiting = true;
RequiresInactiveBeforeCompletion = completionIsAlreadyActive;
HasSeenInactiveCompletionState = !completionIsAlreadyActive;
}
public void MarkCompletionInactive()
{
HasSeenInactiveCompletionState = true;
}
public void End()
{
IsWaiting = false;
RequiresInactiveBeforeCompletion = false;
HasSeenInactiveCompletionState = false;
}
}
}