更新
This commit is contained in:
@@ -2,6 +2,14 @@ namespace ConeCalorimeter.Services;
|
||||
|
||||
internal static class ModbusFloatSelector
|
||||
{
|
||||
private static readonly ModbusFloatByteOrder[] FloatByteOrders =
|
||||
[
|
||||
ModbusFloatByteOrder.Abcd,
|
||||
ModbusFloatByteOrder.Cdab,
|
||||
ModbusFloatByteOrder.Badc,
|
||||
ModbusFloatByteOrder.Dcba
|
||||
];
|
||||
|
||||
private static readonly ModbusFloatByteOrder[] AlternateFloatByteOrders =
|
||||
[
|
||||
ModbusFloatByteOrder.Cdab,
|
||||
@@ -15,22 +23,33 @@ internal static class ModbusFloatSelector
|
||||
double maximum,
|
||||
out ModbusFloatByteOrder byteOrder,
|
||||
out double value,
|
||||
out int matchCount)
|
||||
out int matchCount,
|
||||
ModbusFloatByteOrder? preferredByteOrder = null)
|
||||
{
|
||||
var alternateMatches = AlternateFloatByteOrders
|
||||
var matches = FloatByteOrders
|
||||
.Where(candidate => IsInRange(result.GetValue(candidate), minimum, maximum))
|
||||
.ToArray();
|
||||
var isAbcdInRange = IsInRange(result.Abcd, minimum, maximum);
|
||||
|
||||
matchCount = alternateMatches.Length + (isAbcdInRange ? 1 : 0);
|
||||
matchCount = matches.Length;
|
||||
|
||||
if (isAbcdInRange)
|
||||
if (preferredByteOrder is { } preferred && matches.Contains(preferred))
|
||||
{
|
||||
byteOrder = preferred;
|
||||
value = result.GetValue(byteOrder);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (matches.Contains(ModbusFloatByteOrder.Abcd))
|
||||
{
|
||||
byteOrder = ModbusFloatByteOrder.Abcd;
|
||||
value = result.Abcd;
|
||||
return true;
|
||||
}
|
||||
|
||||
var alternateMatches = matches
|
||||
.Where(candidate => candidate != ModbusFloatByteOrder.Abcd)
|
||||
.ToArray();
|
||||
|
||||
if (alternateMatches.Length != 1)
|
||||
{
|
||||
byteOrder = default;
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace ConeCalorimeter.Services;
|
||||
|
||||
public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
{
|
||||
private const int ScaleCurrentMassStabilityKey = -1;
|
||||
private const double RealtimeZeroTolerance = 0.005;
|
||||
private const int ZeroValueStableReadCount = 2;
|
||||
private const ushort OxygenRegister = 10;
|
||||
private const ushort OrificeFlowRegister = 14;
|
||||
private const ushort OrificePressureRegister = 16;
|
||||
@@ -23,10 +26,21 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
private const ushort IgnitionSecondsRegister = 1014;
|
||||
private const ushort TestSecondsRegister = 1015;
|
||||
private const ushort M3FlameMonitorBit = 3;
|
||||
private static readonly ModbusFloatByteOrder[] RealtimeFloatByteOrders =
|
||||
[
|
||||
ModbusFloatByteOrder.Abcd,
|
||||
ModbusFloatByteOrder.Cdab,
|
||||
ModbusFloatByteOrder.Badc,
|
||||
ModbusFloatByteOrder.Dcba
|
||||
];
|
||||
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
private readonly IScaleService _scaleService;
|
||||
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
|
||||
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
|
||||
private readonly Dictionary<int, double> _stableRealtimeValues = [];
|
||||
private readonly Dictionary<int, int> _pendingZeroReadCounts = [];
|
||||
private ModbusFloatByteOrder? _preferredFloatByteOrder;
|
||||
|
||||
public ModbusRealtimeDataService(
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService,
|
||||
@@ -38,12 +52,18 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
|
||||
public RealtimeSnapshot GetCurrentSnapshot(TimeSpan elapsed)
|
||||
{
|
||||
// Read a non-zero temperature early so the shared realtime path can lock the PLC float word order
|
||||
// before zero-capable fields such as gas concentrations and flow rates are evaluated.
|
||||
var orificeTemperature = ReadRangedFloatOrEmpty("OrificeTemperature", OrificeTemperatureRegister, 200, 700);
|
||||
var coneTemperature = ReadRangedFloatOrEmpty("ConeTemperature", ConeTemperatureRegister, 0, 1200);
|
||||
var sampleTemperature = ReadRangedFloatOrEmpty("SampleTemperature", SampleTemperatureRegister, -50, 1200);
|
||||
|
||||
return new RealtimeSnapshot(
|
||||
OrificeFlow: ReadRangedFloatOrEmpty("OrificeFlow", OrificeFlowRegister, 0, 10),
|
||||
OrificePressure: ReadRangedFloatOrEmpty("OrificePressure", OrificePressureRegister, -5000, 5000),
|
||||
OrificeTemperature: ReadRangedFloatOrEmpty("OrificeTemperature", OrificeTemperatureRegister, 200, 700),
|
||||
ConeTemperature: ReadRangedFloatOrEmpty("ConeTemperature", ConeTemperatureRegister, 0, 1200),
|
||||
SampleTemperature: ReadRangedFloatOrEmpty("SampleTemperature", SampleTemperatureRegister, -50, 1200),
|
||||
OrificeTemperature: orificeTemperature,
|
||||
ConeTemperature: coneTemperature,
|
||||
SampleTemperature: sampleTemperature,
|
||||
Irradiance: ReadRangedFloatOrEmpty("Irradiance", IrradianceRegister, 0, 100),
|
||||
FlameDetected: ReadCoilOrFalse(M3FlameMonitorBit),
|
||||
Oxygen: ReadRangedFloatOrEmpty("O2", OxygenRegister, 0, 30),
|
||||
@@ -72,14 +92,84 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount))
|
||||
if (!TrySelectRealtimeFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount))
|
||||
{
|
||||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
|
||||
return NormalizeRealtimeValue(value);
|
||||
return StabilizeRealtimeValue(registerAddress, value);
|
||||
}
|
||||
|
||||
private bool TrySelectRealtimeFloat(
|
||||
ModbusFloatReadResult result,
|
||||
double minimum,
|
||||
double maximum,
|
||||
out ModbusFloatByteOrder byteOrder,
|
||||
out double value,
|
||||
out int matchCount)
|
||||
{
|
||||
var preferredByteOrder = _preferredFloatByteOrder;
|
||||
if (!ModbusFloatSelector.TrySelectRangedFloat(
|
||||
result,
|
||||
minimum,
|
||||
maximum,
|
||||
out byteOrder,
|
||||
out value,
|
||||
out matchCount,
|
||||
preferredByteOrder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preferredByteOrder.HasValue
|
||||
&& IsZeroLike(value)
|
||||
&& TrySelectSingleNonZeroCandidate(result, minimum, maximum, out var nonZeroByteOrder, out var nonZeroValue))
|
||||
{
|
||||
byteOrder = nonZeroByteOrder;
|
||||
value = nonZeroValue;
|
||||
}
|
||||
|
||||
RememberPreferredFloatByteOrder(byteOrder, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TrySelectSingleNonZeroCandidate(
|
||||
ModbusFloatReadResult result,
|
||||
double minimum,
|
||||
double maximum,
|
||||
out ModbusFloatByteOrder byteOrder,
|
||||
out double value)
|
||||
{
|
||||
var matches = RealtimeFloatByteOrders
|
||||
.Select(candidate => new
|
||||
{
|
||||
ByteOrder = candidate,
|
||||
Value = result.GetValue(candidate)
|
||||
})
|
||||
.Where(candidate => ModbusFloatSelector.IsInRange(candidate.Value, minimum, maximum)
|
||||
&& !IsZeroLike(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 RememberPreferredFloatByteOrder(ModbusFloatByteOrder byteOrder, double value)
|
||||
{
|
||||
if (!IsZeroLike(value))
|
||||
{
|
||||
_preferredFloatByteOrder = byteOrder;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogFloatDiagnostic(
|
||||
@@ -112,7 +202,43 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
|
||||
private static double NormalizeRealtimeValue(double value)
|
||||
{
|
||||
return Math.Abs(value) < 0.005 ? 0 : value;
|
||||
return IsZeroLike(value) ? 0 : value;
|
||||
}
|
||||
|
||||
private static bool IsZeroLike(double value)
|
||||
{
|
||||
return double.IsFinite(value) && Math.Abs(value) < RealtimeZeroTolerance;
|
||||
}
|
||||
|
||||
private double StabilizeRealtimeValue(int key, double value)
|
||||
{
|
||||
var normalizedValue = NormalizeRealtimeValue(value);
|
||||
|
||||
if (!_stableRealtimeValues.TryGetValue(key, out var stableValue))
|
||||
{
|
||||
AcceptStableRealtimeValue(key, normalizedValue);
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
if (IsZeroLike(normalizedValue) && !IsZeroLike(stableValue))
|
||||
{
|
||||
var pendingCount = _pendingZeroReadCounts.GetValueOrDefault(key) + 1;
|
||||
_pendingZeroReadCounts[key] = pendingCount;
|
||||
|
||||
if (pendingCount < ZeroValueStableReadCount)
|
||||
{
|
||||
return stableValue;
|
||||
}
|
||||
}
|
||||
|
||||
AcceptStableRealtimeValue(key, normalizedValue);
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
private void AcceptStableRealtimeValue(int key, double value)
|
||||
{
|
||||
_stableRealtimeValues[key] = value;
|
||||
_pendingZeroReadCounts.Remove(key);
|
||||
}
|
||||
|
||||
private int ReadInt16OrEmpty(ushort registerAddress)
|
||||
@@ -125,7 +251,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
private double ReadScaleCurrentMassOrEmpty()
|
||||
{
|
||||
return _scaleService.TryReadCurrentMass(out var value)
|
||||
? NormalizeRealtimeValue(value)
|
||||
? StabilizeRealtimeValue(ScaleCurrentMassStabilityKey, value)
|
||||
: double.NaN;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -42,6 +43,12 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
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,
|
||||
@@ -61,6 +68,9 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
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 = "";
|
||||
@@ -191,37 +201,51 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
ToggleHoldAction(action);
|
||||
}
|
||||
|
||||
private void RefreshDeviceValues()
|
||||
private CalibrationDataReadStatus RefreshDeviceValues()
|
||||
{
|
||||
BaselineOxygenText = ReadOxygenPercentText(
|
||||
"BaselineOxygen",
|
||||
BaselineOxygenRegister,
|
||||
"0.00");
|
||||
TemperatureText = ReadRangedFloatText(
|
||||
var temperature = ReadRangedFloatDisplay(
|
||||
"Temperature",
|
||||
"Te",
|
||||
TemperatureRegister,
|
||||
TemperatureMinimum,
|
||||
TemperatureMaximum,
|
||||
"0.00");
|
||||
PressureDifferenceText = ReadRangedFloatText(
|
||||
TemperatureText = temperature.Text;
|
||||
RememberCalibrationFloatByteOrder(temperature);
|
||||
|
||||
var pressureDifference = ReadRangedFloatDisplay(
|
||||
"PressureDifference",
|
||||
"Δp",
|
||||
PressureDifferenceRegister,
|
||||
PressureDifferenceMinimum,
|
||||
PressureDifferenceMaximum,
|
||||
"0.00");
|
||||
CalibrationOxygenText = ReadRangedFloatText(
|
||||
PressureDifferenceText = pressureDifference.Text;
|
||||
|
||||
var calibrationOxygen = ReadOxygenPercentDisplay(
|
||||
"CalibrationOxygen",
|
||||
"XO2",
|
||||
CalibrationOxygenRegister,
|
||||
OxygenMinimum,
|
||||
OxygenMaximum,
|
||||
"0.00");
|
||||
CValueText = ReadRangedFloatText(
|
||||
|
||||
CalibrationOxygenText = calibrationOxygen.Text;
|
||||
RememberCalibrationFloatByteOrder(calibrationOxygen);
|
||||
|
||||
var cValue = ReadRangedFloatDisplay(
|
||||
"CValue",
|
||||
"C",
|
||||
CValueRegister,
|
||||
CValueMinimum,
|
||||
CValueMaximum,
|
||||
"0.00");
|
||||
CValueText = cValue.Text;
|
||||
|
||||
RefreshHoldActionStates();
|
||||
return new CalibrationDataReadStatus(pressureDifference, calibrationOxygen, cValue);
|
||||
}
|
||||
|
||||
private string ReadRangedFloatText(
|
||||
@@ -230,44 +254,82 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
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))
|
||||
{
|
||||
return string.Empty;
|
||||
LogFloatReadFailure(label, registerAddress, "读取失败");
|
||||
return DisplayValueReadResult.Failed(displayLabel, "读取失败");
|
||||
}
|
||||
|
||||
if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount))
|
||||
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);
|
||||
return string.Empty;
|
||||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount, preferredByteOrder);
|
||||
return DisplayValueReadResult.Failed(displayLabel, "无有效值");
|
||||
}
|
||||
|
||||
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
|
||||
return NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture);
|
||||
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))
|
||||
{
|
||||
return string.Empty;
|
||||
LogFloatReadFailure(label, registerAddress, "读取失败");
|
||||
return DisplayValueReadResult.Failed(displayLabel, "读取失败");
|
||||
}
|
||||
|
||||
if (!TrySelectOxygenPercent(result, out var byteOrder, out var value, out var matchCount))
|
||||
var preferredByteOrder = _calibrationFloatByteOrder;
|
||||
if (!TrySelectOxygenPercent(result, preferredByteOrder, out var byteOrder, out var value, out var matchCount))
|
||||
{
|
||||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
|
||||
return string.Empty;
|
||||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount, preferredByteOrder);
|
||||
return DisplayValueReadResult.Failed(displayLabel, "无有效值");
|
||||
}
|
||||
|
||||
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
|
||||
return NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture);
|
||||
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)
|
||||
@@ -279,8 +341,15 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
|
||||
matchCount = candidates.Length;
|
||||
|
||||
var selected = candidates.FirstOrDefault(
|
||||
candidate => candidate.ByteOrder == ModbusFloatByteOrder.Abcd && candidate.IsPlausible);
|
||||
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)
|
||||
{
|
||||
@@ -294,6 +363,13 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -351,8 +427,21 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
ushort registerAddress,
|
||||
ModbusFloatReadResult result,
|
||||
ModbusFloatByteOrder? selectedByteOrder,
|
||||
int matchCount)
|
||||
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))
|
||||
@@ -365,13 +454,66 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
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}.");
|
||||
+ $"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)
|
||||
@@ -525,7 +667,7 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
continue;
|
||||
}
|
||||
|
||||
CompletePairedAction(waitState, actionViewModel);
|
||||
CompletePairedAction(action, waitState, actionViewModel);
|
||||
}
|
||||
|
||||
if (!hasWaitingAction)
|
||||
@@ -535,10 +677,14 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
}
|
||||
|
||||
private void CompletePairedAction(
|
||||
string action,
|
||||
PairedActionWaitState waitState,
|
||||
CValueCalibrationActionViewModel actionViewModel)
|
||||
{
|
||||
LastAction = waitState.CompletedText;
|
||||
var calibrationDataStatus = RefreshDeviceValues();
|
||||
LastAction = action == CalibrationStartAction && !calibrationDataStatus.HasAllValues
|
||||
? $"{waitState.CompletedText},{calibrationDataStatus.MissingSummary}"
|
||||
: waitState.CompletedText;
|
||||
waitState.End();
|
||||
|
||||
try
|
||||
@@ -633,6 +779,55 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user