更新20260122
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user