Files
ConeCalorimeter/ConeCalorimeter/ViewModels/CValueCalibrationViewModel.cs
GukSang.Jin 1d087c00e8 更新2026
2026-05-27 11:50:35 +08:00

926 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}