926 lines
31 KiB
C#
926 lines
31 KiB
C#
using System.Collections.ObjectModel;
|
||
using System.Diagnostics;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
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 double DisplayZeroTolerance = 0.00005;
|
||
private const string FourDecimalDisplayFormat = "0.0000";
|
||
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 TimeSpan DiagnosticLogInterval = TimeSpan.FromSeconds(5);
|
||
private static readonly string DiagnosticLogFilePath = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"ConeCalorimeter",
|
||
"Logs",
|
||
"cvalue-calibration-diagnostics.log");
|
||
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 readonly Dictionary<ushort, string> _lastFileDiagnosticByRegister = [];
|
||
private readonly Dictionary<ushort, DateTime> _lastFileDiagnosticAtByRegister = [];
|
||
private ModbusFloatByteOrder? _calibrationFloatByteOrder;
|
||
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, showStateText: false),
|
||
CreateAction(MethaneValveAction, MethaneValveAction, MethaneValveAction, showStateText: false),
|
||
CreateAction(FanAction, FanAction, FanAction, showStateText: false),
|
||
CreateAction(IgniterAction, IgniterAction, IgniterAction, showStateText: false),
|
||
CreateAction(SamplingPumpAction, SamplingPumpAction, SamplingPumpAction, showStateText: false)
|
||
];
|
||
|
||
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.0000";
|
||
|
||
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 CalibrationDataReadStatus RefreshDeviceValues()
|
||
{
|
||
BaselineOxygenText = ReadOxygenPercentText(
|
||
"BaselineOxygen",
|
||
BaselineOxygenRegister,
|
||
FourDecimalDisplayFormat);
|
||
var temperature = ReadRangedFloatDisplay(
|
||
"Temperature",
|
||
"Te",
|
||
TemperatureRegister,
|
||
TemperatureMinimum,
|
||
TemperatureMaximum,
|
||
FourDecimalDisplayFormat);
|
||
TemperatureText = temperature.Text;
|
||
RememberCalibrationFloatByteOrder(temperature);
|
||
|
||
var pressureDifference = ReadRangedFloatDisplay(
|
||
"PressureDifference",
|
||
"Δp",
|
||
PressureDifferenceRegister,
|
||
PressureDifferenceMinimum,
|
||
PressureDifferenceMaximum,
|
||
FourDecimalDisplayFormat);
|
||
PressureDifferenceText = pressureDifference.Text;
|
||
|
||
var calibrationOxygen = ReadOxygenPercentDisplay(
|
||
"CalibrationOxygen",
|
||
"XO2",
|
||
CalibrationOxygenRegister,
|
||
FourDecimalDisplayFormat);
|
||
|
||
CalibrationOxygenText = calibrationOxygen.Text;
|
||
RememberCalibrationFloatByteOrder(calibrationOxygen);
|
||
|
||
var cValue = ReadRangedFloatDisplay(
|
||
"CValue",
|
||
"C",
|
||
CValueRegister,
|
||
CValueMinimum,
|
||
CValueMaximum,
|
||
FourDecimalDisplayFormat);
|
||
CValueText = cValue.Text;
|
||
|
||
RefreshHoldActionStates();
|
||
return new CalibrationDataReadStatus(pressureDifference, calibrationOxygen, cValue);
|
||
}
|
||
|
||
private string ReadRangedFloatText(
|
||
string label,
|
||
ushort registerAddress,
|
||
double minimum,
|
||
double maximum,
|
||
string format)
|
||
{
|
||
return ReadRangedFloatDisplay(label, label, registerAddress, minimum, maximum, format).Text;
|
||
}
|
||
|
||
private DisplayValueReadResult ReadRangedFloatDisplay(
|
||
string label,
|
||
string displayLabel,
|
||
ushort registerAddress,
|
||
double minimum,
|
||
double maximum,
|
||
string format)
|
||
{
|
||
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
|
||
{
|
||
LogFloatReadFailure(label, registerAddress, "读取失败");
|
||
return DisplayValueReadResult.Failed(displayLabel, "读取失败");
|
||
}
|
||
|
||
var preferredByteOrder = _calibrationFloatByteOrder;
|
||
if (!ModbusFloatSelector.TrySelectRangedFloat(
|
||
result,
|
||
minimum,
|
||
maximum,
|
||
out var byteOrder,
|
||
out var value,
|
||
out var matchCount,
|
||
preferredByteOrder))
|
||
{
|
||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount, preferredByteOrder);
|
||
return DisplayValueReadResult.Failed(displayLabel, "无有效值");
|
||
}
|
||
|
||
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount, preferredByteOrder);
|
||
return DisplayValueReadResult.Valid(
|
||
displayLabel,
|
||
NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture),
|
||
byteOrder);
|
||
}
|
||
|
||
private string ReadOxygenPercentText(
|
||
string label,
|
||
ushort registerAddress,
|
||
string format)
|
||
{
|
||
return ReadOxygenPercentDisplay(label, label, registerAddress, format).Text;
|
||
}
|
||
|
||
private DisplayValueReadResult ReadOxygenPercentDisplay(
|
||
string label,
|
||
string displayLabel,
|
||
ushort registerAddress,
|
||
string format)
|
||
{
|
||
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
|
||
{
|
||
LogFloatReadFailure(label, registerAddress, "读取失败");
|
||
return DisplayValueReadResult.Failed(displayLabel, "读取失败");
|
||
}
|
||
|
||
var preferredByteOrder = _calibrationFloatByteOrder;
|
||
if (!TrySelectOxygenPercent(result, preferredByteOrder, out var byteOrder, out var value, out var matchCount))
|
||
{
|
||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount, preferredByteOrder);
|
||
return DisplayValueReadResult.Failed(displayLabel, "无有效值");
|
||
}
|
||
|
||
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount, preferredByteOrder);
|
||
return DisplayValueReadResult.Valid(
|
||
displayLabel,
|
||
NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture),
|
||
byteOrder);
|
||
}
|
||
|
||
private static bool TrySelectOxygenPercent(
|
||
ModbusFloatReadResult result,
|
||
ModbusFloatByteOrder? preferredByteOrder,
|
||
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 = preferredByteOrder is { } plausiblePreferred
|
||
? candidates.FirstOrDefault(candidate => candidate.ByteOrder == plausiblePreferred && candidate.IsPlausible)
|
||
: default;
|
||
|
||
if (!selected.IsValid)
|
||
{
|
||
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 = preferredByteOrder is { } validPreferred
|
||
? candidates.FirstOrDefault(candidate => candidate.ByteOrder == validPreferred)
|
||
: default;
|
||
}
|
||
|
||
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,
|
||
ModbusFloatByteOrder? preferredByteOrder)
|
||
{
|
||
var selectedText = selectedByteOrder?.ToString() ?? "none";
|
||
var preferredText = preferredByteOrder?.ToString() ?? "none";
|
||
var statusText = selectedByteOrder is null ? "无有效值" : "读取成功";
|
||
var diagnosticMessage =
|
||
$"CValueCalibration label={label} register=D{registerAddress} endpoint={_tcpDeviceConnectionService.Endpoint} "
|
||
+ $"status={statusText} raw=[{result.RawHex}] "
|
||
+ $"ABCD={FormatDiagnosticFloat(result.Abcd)} CDAB={FormatDiagnosticFloat(result.Cdab)} "
|
||
+ $"BADC={FormatDiagnosticFloat(result.Badc)} DCBA={FormatDiagnosticFloat(result.Dcba)} "
|
||
+ $"preferred={preferredText} selected={selectedText} matches={matchCount}";
|
||
|
||
WriteFloatDiagnosticToFile(registerAddress, diagnosticMessage);
|
||
|
||
if (selectedByteOrder is null)
|
||
{
|
||
if (!_loggedInvalidFloatDiagnostics.Add(registerAddress))
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
else if (!_loggedFloatDiagnostics.Add(registerAddress))
|
||
{
|
||
return;
|
||
}
|
||
|
||
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}, "
|
||
+ $"preferred={preferredText}, selected={selectedText}, matches={matchCount}.");
|
||
}
|
||
|
||
private void LogFloatReadFailure(string label, ushort registerAddress, string statusText)
|
||
{
|
||
var diagnosticMessage =
|
||
$"CValueCalibration label={label} register=D{registerAddress} endpoint={_tcpDeviceConnectionService.Endpoint} "
|
||
+ $"status={statusText} connection=\"{_tcpDeviceConnectionService.StatusText}\"";
|
||
|
||
WriteFloatDiagnosticToFile(registerAddress, diagnosticMessage);
|
||
Debug.WriteLine($"C value calibration float {label} register {registerAddress} {statusText}.");
|
||
}
|
||
|
||
private void WriteFloatDiagnosticToFile(ushort registerAddress, string message)
|
||
{
|
||
var now = DateTime.Now;
|
||
if (_lastFileDiagnosticByRegister.TryGetValue(registerAddress, out var lastMessage)
|
||
&& string.Equals(lastMessage, message, StringComparison.Ordinal)
|
||
&& _lastFileDiagnosticAtByRegister.TryGetValue(registerAddress, out var lastLoggedAt)
|
||
&& now - lastLoggedAt < DiagnosticLogInterval)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_lastFileDiagnosticByRegister[registerAddress] = message;
|
||
_lastFileDiagnosticAtByRegister[registerAddress] = now;
|
||
|
||
try
|
||
{
|
||
var directory = Path.GetDirectoryName(DiagnosticLogFilePath);
|
||
if (!string.IsNullOrWhiteSpace(directory))
|
||
{
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
File.AppendAllText(
|
||
DiagnosticLogFilePath,
|
||
$"{now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"C value calibration diagnostic log write failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void RememberCalibrationFloatByteOrder(DisplayValueReadResult result)
|
||
{
|
||
if (result.ByteOrder is { } byteOrder)
|
||
{
|
||
_calibrationFloatByteOrder = byteOrder;
|
||
}
|
||
}
|
||
|
||
private static string FormatDiagnosticFloat(double value)
|
||
{
|
||
return value.ToString("G9", CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
private static double NormalizeDisplayValue(double value)
|
||
{
|
||
return Math.Abs(value) < DisplayZeroTolerance ? 0 : value;
|
||
}
|
||
|
||
private CValueCalibrationActionViewModel CreateAction(
|
||
string label,
|
||
string inactiveDisplayText,
|
||
string activeDisplayText,
|
||
bool showStateText = true)
|
||
{
|
||
var action = new CValueCalibrationActionViewModel(
|
||
label,
|
||
ActionCommand,
|
||
inactiveDisplayText,
|
||
activeDisplayText,
|
||
showStateText);
|
||
_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}控制完成";
|
||
if (!UpdateActionStatus(action, coilAddress) && _actionsByLabel.TryGetValue(action, out var actionViewModel))
|
||
{
|
||
actionViewModel.UpdateStatus(nextValue);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
LastAction = $"{action}控制失败";
|
||
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(action, waitState, actionViewModel);
|
||
}
|
||
|
||
if (!hasWaitingAction)
|
||
{
|
||
_pairedActionCompletionTimer.Stop();
|
||
}
|
||
}
|
||
|
||
private void CompletePairedAction(
|
||
string action,
|
||
PairedActionWaitState waitState,
|
||
CValueCalibrationActionViewModel actionViewModel)
|
||
{
|
||
var calibrationDataStatus = RefreshDeviceValues();
|
||
var shutdownSummary = action == CalibrationStartAction
|
||
? CloseCalibrationActuatorsAfterCompletion()
|
||
: string.Empty;
|
||
LastAction = action == CalibrationStartAction && !calibrationDataStatus.HasAllValues
|
||
? AppendActionSummary(waitState.CompletedText, calibrationDataStatus.MissingSummary, shutdownSummary)
|
||
: AppendActionSummary(waitState.CompletedText, shutdownSummary);
|
||
waitState.End();
|
||
|
||
try
|
||
{
|
||
waitState.CompletedAction();
|
||
}
|
||
finally
|
||
{
|
||
actionViewModel.FinishWaiting();
|
||
}
|
||
}
|
||
|
||
private string CloseCalibrationActuatorsAfterCompletion()
|
||
{
|
||
var failedActions = new List<string>(2);
|
||
|
||
CloseHoldActionAfterCompletion(MethaneValveAction, MethaneValveCoil, failedActions);
|
||
CloseHoldActionAfterCompletion(SamplingPumpAction, SamplingPumpCoil, failedActions);
|
||
|
||
return failedActions.Count == 0
|
||
? "已关闭甲烷阀和取样泵"
|
||
: $"{string.Join("、", failedActions)}关闭失败";
|
||
}
|
||
|
||
private void CloseHoldActionAfterCompletion(
|
||
string action,
|
||
ushort coilAddress,
|
||
List<string> failedActions)
|
||
{
|
||
if (_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false))
|
||
{
|
||
if (!UpdateActionStatus(action, coilAddress) && _actionsByLabel.TryGetValue(action, out var actionViewModel))
|
||
{
|
||
actionViewModel.UpdateStatus(false);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
failedActions.Add(action);
|
||
Debug.WriteLine($"C value calibration action '{action}' auto close failed after calibration completion.");
|
||
}
|
||
|
||
private static string AppendActionSummary(string message, params string[] summaries)
|
||
{
|
||
foreach (var summary in summaries)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(summary))
|
||
{
|
||
message = $"{message},{summary}";
|
||
}
|
||
}
|
||
|
||
return message;
|
||
}
|
||
|
||
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 readonly record struct DisplayValueReadResult(
|
||
string DisplayLabel,
|
||
string Text,
|
||
bool HasValue,
|
||
string FailureDescription,
|
||
ModbusFloatByteOrder? ByteOrder)
|
||
{
|
||
public static DisplayValueReadResult Valid(
|
||
string displayLabel,
|
||
string text,
|
||
ModbusFloatByteOrder byteOrder)
|
||
{
|
||
return new DisplayValueReadResult(displayLabel, text, true, string.Empty, byteOrder);
|
||
}
|
||
|
||
public static DisplayValueReadResult Failed(string displayLabel, string failureDescription)
|
||
{
|
||
return new DisplayValueReadResult(displayLabel, string.Empty, false, failureDescription, null);
|
||
}
|
||
}
|
||
|
||
private readonly record struct CalibrationDataReadStatus(
|
||
DisplayValueReadResult PressureDifference,
|
||
DisplayValueReadResult CalibrationOxygen,
|
||
DisplayValueReadResult CValue)
|
||
{
|
||
public bool HasAllValues => PressureDifference.HasValue && CalibrationOxygen.HasValue && CValue.HasValue;
|
||
|
||
public string MissingSummary
|
||
{
|
||
get
|
||
{
|
||
var failures = new List<string>(3);
|
||
AddFailure(failures, PressureDifference);
|
||
AddFailure(failures, CalibrationOxygen);
|
||
AddFailure(failures, CValue);
|
||
return string.Join(";", failures);
|
||
}
|
||
}
|
||
|
||
private static void AddFailure(List<string> failures, DisplayValueReadResult result)
|
||
{
|
||
if (!result.HasValue)
|
||
{
|
||
failures.Add($"{result.DisplayLabel}{result.FailureDescription}");
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|