更新20260122

This commit is contained in:
GukSang.Jin
2026-06-12 10:56:30 +08:00
parent 04b401e7b8
commit 424a2d8b2d
2 changed files with 235 additions and 156 deletions

View File

@@ -103,6 +103,7 @@ public sealed class MainWindowViewModel : ObservableObject
private const double MinimumTorqueChangeThreshold = 0.05;
private const string TorqueUnit = "mN.m";
private static readonly TimeSpan RealtimeRefreshInterval = TimeSpan.FromMilliseconds(100);
private static readonly TimeSpan RealtimeDataFreshnessTimeout = TimeSpan.FromSeconds(3);
private static readonly TimeSpan SnapshotInterval = TimeSpan.FromSeconds(5);
private static readonly TimeSpan NoLoadCaptureDuration = TimeSpan.FromSeconds(3);
private static readonly TimeSpan SpeedTorqueStartConfirmationTimeout = TimeSpan.FromSeconds(5);
@@ -220,6 +221,7 @@ public sealed class MainWindowViewModel : ObservableObject
private bool _isReadingParameterConfig;
private bool _hasLoadedParameterConfigFromPlc;
private bool _lastRealtimeReadFailed;
private DateTime _lastSuccessfulRealtimeReadAt = DateTime.MinValue;
private bool _isAutoStoppingDisplacement;
private bool _isAutoStoppingSpeedTorque;
private bool _isApplyingParameterConfigToInputs;
@@ -752,11 +754,10 @@ public sealed class MainWindowViewModel : ObservableObject
try
{
PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig();
Task<IReadOnlyDictionary<ushort, float>> registerReadTask =
_plcRegisterService.ReadFloatValuesAsync(config, RealtimeRegisterAddresses);
Task<IReadOnlyDictionary<ushort, bool>> coilReadTask =
_plcCoilService.ReadCoilValuesAsync(config, RealtimeCoilAddresses);
IReadOnlyDictionary<ushort, float> values = await registerReadTask;
IReadOnlyDictionary<ushort, float> values =
await _plcRegisterService.ReadFloatValuesAsync(config, RealtimeRegisterAddresses);
IReadOnlyDictionary<ushort, bool> coilValues =
await _plcCoilService.ReadCoilValuesAsync(config, RealtimeCoilAddresses);
double dialIndicator = ReadFloatValue(values, DialIndicatorRegister, "千分表显示");
double axialForce = ReadFloatValue(values, AxialForceDisplayRegister, "轴向力显示");
@@ -788,12 +789,12 @@ public sealed class MainWindowViewModel : ObservableObject
UpdateSpeedTorqueDisplay();
UpdateNoLoadSpeedDisplay();
IReadOnlyDictionary<ushort, bool> coilValues = await coilReadTask;
ApplyResetCoilValues(coilValues);
ApplyAxialForceModeState(ReadCoilValue(coilValues, AxialForceModeCoil));
bool isVentValveOpen = ReadCoilValue(coilValues, VentValveCoil);
VentValveButtonText = isVentValveOpen ? "关闭气阀" : "通气阀";
_lastSuccessfulRealtimeReadAt = DateTime.Now;
CaptureRealtimeSample(dialIndicator, coilValues);
FinalizeNoLoadSpeedRunIfDue();
QueueSnapshotIfDue();
@@ -837,7 +838,7 @@ public sealed class MainWindowViewModel : ObservableObject
{
if (!_lastRealtimeReadFailed)
{
StatusText = $"实时数据读取失败:{OperatorMessageFormatter.FromException(ex)}";
StatusText = $"实时数据读取失败,当前读数为上次有效值{OperatorMessageFormatter.FromException(ex)}";
_lastRealtimeReadFailed = true;
Log.Warning(ex, "PLC实时数据读取失败后续相同故障将等待恢复后再记录");
}
@@ -2433,6 +2434,12 @@ public sealed class MainWindowViewModel : ObservableObject
return;
}
if (!TryGetRealtimeSpeed(out _))
{
StatusText = "实时数据未更新,已阻止空载转速记录。";
return;
}
try
{
await _plcCoilService.WriteCoilAsync(
@@ -3089,13 +3096,13 @@ public sealed class MainWindowViewModel : ObservableObject
private bool TryGetDialValue(out double value)
{
value = _dialIndicator;
return true;
return HasFreshRealtimeData() && double.IsFinite(value);
}
private bool TryGetAxialForceValue(out double value)
{
value = _axialForce;
return true;
return HasFreshRealtimeData() && double.IsFinite(value);
}
private void UpdateRealtimeSpeedFromInput()
@@ -3244,13 +3251,19 @@ public sealed class MainWindowViewModel : ObservableObject
private bool TryGetRealtimeSpeed(out double value)
{
value = _realtimeSpeed;
return true;
return HasFreshRealtimeData() && double.IsFinite(value);
}
private bool TryGetRealtimeTorque(out double value)
{
value = _realtimeTorque;
return true;
return HasFreshRealtimeData() && double.IsFinite(value);
}
private bool HasFreshRealtimeData()
{
return _lastSuccessfulRealtimeReadAt != DateTime.MinValue
&& DateTime.Now - _lastSuccessfulRealtimeReadAt <= RealtimeDataFreshnessTimeout;
}
private void ApplyResetCoilValues(IReadOnlyDictionary<ushort, bool> coilValues)
@@ -3482,10 +3495,17 @@ public sealed class MainWindowViewModel : ObservableObject
}
private int GetTorqueSampleLimit()
{
if (_parameterConfig.TorqueHoldTime <= 0)
{
return MaxTorqueSampleCount;
}
int requiredSamples = (int)Math.Ceiling(
_parameterConfig.TorqueHoldTime / RealtimeRefreshInterval.TotalSeconds) + 2;
return Math.Max(MaxTorqueSampleCount, requiredSamples);
}
private void CaptureRealtimeSample(double dialIndicator, IReadOnlyDictionary<ushort, bool> coilValues)
{
var sample = new RealtimeSamplePayload

View File

@@ -34,6 +34,7 @@ public interface IPlcRegisterService
public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterService
{
private readonly object _transactionLock = new();
private readonly SemaphoreSlim _transportLock = new(1, 1);
private ushort _transactionId;
public async Task PulseCoilAsync(PlcConnectionConfig config, ushort coilAddress, CancellationToken cancellationToken = default)
@@ -94,10 +95,16 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
}
IReadOnlyList<RegisterReadGroup> groups = CreateRegisterReadGroups(registerAddresses);
Task<ushort[]>[] readTasks = groups
.Select(group => ReadHoldingRegistersAsync(config, group.StartAddress, group.RegisterCount, cancellationToken))
.ToArray();
ushort[][] groupRegisters = await Task.WhenAll(readTasks);
var groupRegisters = new ushort[groups.Count][];
for (int groupIndex = 0; groupIndex < groups.Count; groupIndex++)
{
RegisterReadGroup group = groups[groupIndex];
groupRegisters[groupIndex] = await ReadHoldingRegistersAsync(
config,
group.StartAddress,
group.RegisterCount,
cancellationToken);
}
var values = new Dictionary<ushort, float>();
for (int groupIndex = 0; groupIndex < groups.Count; groupIndex++)
@@ -198,6 +205,9 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
}
private async Task WriteSingleCoilAsync(PlcConnectionConfig config, ushort coilAddress, bool value, CancellationToken cancellationToken)
{
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@@ -212,11 +222,7 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
byte[] response = new byte[12];
await ReadExactlyAsync(stream, response, timeoutCts.Token);
if (response[0] != request[0] || response[1] != request[1])
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
}
ValidateFixedResponseHeader(response, request);
if (response[7] == 0x85)
{
@@ -227,9 +233,22 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{response[7]:X2}。");
}
if (!response.AsSpan(8, 4).SequenceEqual(request.AsSpan(8, 4)))
{
throw new InvalidOperationException("PLC 写线圈响应地址或值不匹配。");
}
}
finally
{
_transportLock.Release();
}
}
private async Task<ushort[]> ReadHoldingRegistersAsync(PlcConnectionConfig config, ushort startAddress, ushort numberOfPoints, CancellationToken cancellationToken)
{
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@@ -244,20 +263,11 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
byte[] header = new byte[7];
await ReadExactlyAsync(stream, header, timeoutCts.Token);
int remainingLength = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2)) - 1;
if (remainingLength <= 0)
{
throw new InvalidOperationException("PLC 响应长度无效。");
}
int remainingLength = ValidateReadResponseHeader(header, request);
byte[] pdu = new byte[remainingLength];
await ReadExactlyAsync(stream, pdu, timeoutCts.Token);
if (header[0] != request[0] || header[1] != request[1])
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
}
if (pdu[0] == 0x83)
{
byte exceptionCode = pdu.Length > 1 ? pdu[1] : (byte)0;
@@ -283,8 +293,16 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
return registers;
}
finally
{
_transportLock.Release();
}
}
private async Task<bool[]> ReadCoilsAsync(PlcConnectionConfig config, ushort startAddress, ushort numberOfPoints, CancellationToken cancellationToken)
{
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@@ -299,20 +317,11 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
byte[] header = new byte[7];
await ReadExactlyAsync(stream, header, timeoutCts.Token);
int remainingLength = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2)) - 1;
if (remainingLength <= 0)
{
throw new InvalidOperationException("PLC 响应长度无效。");
}
int remainingLength = ValidateReadResponseHeader(header, request);
byte[] pdu = new byte[remainingLength];
await ReadExactlyAsync(stream, pdu, timeoutCts.Token);
if (header[0] != request[0] || header[1] != request[1])
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
}
if (pdu[0] == 0x81)
{
byte exceptionCode = pdu.Length > 1 ? pdu[1] : (byte)0;
@@ -339,8 +348,16 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
return coils;
}
finally
{
_transportLock.Release();
}
}
private async Task WriteMultipleRegistersAsync(PlcConnectionConfig config, ushort startAddress, ushort[] values, CancellationToken cancellationToken)
{
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@@ -355,11 +372,7 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
byte[] response = new byte[12];
await ReadExactlyAsync(stream, response, timeoutCts.Token);
if (response[0] != request[0] || response[1] != request[1])
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
}
ValidateFixedResponseHeader(response, request);
if (response[7] == 0x90)
{
@@ -378,6 +391,52 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
throw new InvalidOperationException("PLC 写寄存器响应地址或数量不匹配。");
}
}
finally
{
_transportLock.Release();
}
}
private static int ValidateReadResponseHeader(byte[] header, byte[] request)
{
ValidateMbapHeader(header, request);
int remainingLength = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2)) - 1;
if (remainingLength <= 0)
{
throw new InvalidOperationException("PLC 响应长度无效。");
}
return remainingLength;
}
private static void ValidateFixedResponseHeader(byte[] response, byte[] request)
{
ValidateMbapHeader(response, request);
if (BinaryPrimitives.ReadUInt16BigEndian(response.AsSpan(4, 2)) != 6)
{
throw new InvalidOperationException("PLC 响应长度无效。");
}
}
private static void ValidateMbapHeader(ReadOnlySpan<byte> response, ReadOnlySpan<byte> request)
{
if (response.Length < 7
|| response[0] != request[0]
|| response[1] != request[1])
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
}
if (response[2] != 0 || response[3] != 0)
{
throw new InvalidOperationException("PLC 响应协议标识无效。");
}
if (response[6] != request[6])
{
throw new InvalidOperationException("PLC 响应站号不匹配。");
}
}
private static IReadOnlyList<RegisterReadGroup> CreateRegisterReadGroups(IReadOnlyCollection<ushort> registerAddresses)
{