更新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)
@@ -3483,7 +3496,14 @@ public sealed class MainWindowViewModel : ObservableObject
private int GetTorqueSampleLimit()
{
return MaxTorqueSampleCount;
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)

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++)
@@ -199,183 +206,235 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
private async Task WriteSingleCoilAsync(PlcConnectionConfig config, ushort coilAddress, bool value, CancellationToken cancellationToken)
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildWriteSingleCoilRequest(transactionId, config.UnitId, coilAddress, value);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] response = new byte[12];
await ReadExactlyAsync(stream, response, timeoutCts.Token);
if (response[0] != request[0] || response[1] != request[1])
await _transportLock.WaitAsync(cancellationToken);
try
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildWriteSingleCoilRequest(transactionId, config.UnitId, coilAddress, value);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] response = new byte[12];
await ReadExactlyAsync(stream, response, timeoutCts.Token);
ValidateFixedResponseHeader(response, request);
if (response[7] == 0x85)
{
throw new InvalidOperationException($"PLC 写线圈异常,功能码 0x05异常码 0x{response[8]:X2}。");
}
if (response[7] != 0x05)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{response[7]:X2}。");
}
if (!response.AsSpan(8, 4).SequenceEqual(request.AsSpan(8, 4)))
{
throw new InvalidOperationException("PLC 写线圈响应地址或值不匹配。");
}
}
if (response[7] == 0x85)
finally
{
throw new InvalidOperationException($"PLC 写线圈异常,功能码 0x05异常码 0x{response[8]:X2}。");
}
if (response[7] != 0x05)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{response[7]:X2}。");
_transportLock.Release();
}
}
private async Task<ushort[]> ReadHoldingRegistersAsync(PlcConnectionConfig config, ushort startAddress, ushort numberOfPoints, CancellationToken cancellationToken)
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildReadHoldingRegistersRequest(transactionId, config.UnitId, startAddress, numberOfPoints);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] header = new byte[7];
await ReadExactlyAsync(stream, header, timeoutCts.Token);
int remainingLength = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2)) - 1;
if (remainingLength <= 0)
await _transportLock.WaitAsync(cancellationToken);
try
{
throw new InvalidOperationException("PLC 响应长度无效。");
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildReadHoldingRegistersRequest(transactionId, config.UnitId, startAddress, numberOfPoints);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] header = new byte[7];
await ReadExactlyAsync(stream, header, timeoutCts.Token);
int remainingLength = ValidateReadResponseHeader(header, request);
byte[] pdu = new byte[remainingLength];
await ReadExactlyAsync(stream, pdu, timeoutCts.Token);
if (pdu[0] == 0x83)
{
byte exceptionCode = pdu.Length > 1 ? pdu[1] : (byte)0;
throw new InvalidOperationException($"PLC 读寄存器异常,功能码 0x03异常码 0x{exceptionCode:X2}。");
}
if (pdu[0] != 0x03)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{pdu[0]:X2}。");
}
int byteCount = pdu[1];
if (byteCount != numberOfPoints * 2 || pdu.Length < 2 + byteCount)
{
throw new InvalidOperationException("PLC 读寄存器响应字节数无效。");
}
ushort[] registers = new ushort[numberOfPoints];
for (int i = 0; i < registers.Length; i++)
{
registers[i] = BinaryPrimitives.ReadUInt16BigEndian(pdu.AsSpan(2 + i * 2, 2));
}
return registers;
}
byte[] pdu = new byte[remainingLength];
await ReadExactlyAsync(stream, pdu, timeoutCts.Token);
if (header[0] != request[0] || header[1] != request[1])
finally
{
throw new InvalidOperationException("PLC 响应事务号不匹配。");
_transportLock.Release();
}
if (pdu[0] == 0x83)
{
byte exceptionCode = pdu.Length > 1 ? pdu[1] : (byte)0;
throw new InvalidOperationException($"PLC 读寄存器异常,功能码 0x03异常码 0x{exceptionCode:X2}。");
}
if (pdu[0] != 0x03)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{pdu[0]:X2}。");
}
int byteCount = pdu[1];
if (byteCount != numberOfPoints * 2 || pdu.Length < 2 + byteCount)
{
throw new InvalidOperationException("PLC 读寄存器响应字节数无效。");
}
ushort[] registers = new ushort[numberOfPoints];
for (int i = 0; i < registers.Length; i++)
{
registers[i] = BinaryPrimitives.ReadUInt16BigEndian(pdu.AsSpan(2 + i * 2, 2));
}
return registers;
}
private async Task<bool[]> ReadCoilsAsync(PlcConnectionConfig config, ushort startAddress, ushort numberOfPoints, CancellationToken cancellationToken)
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildReadCoilsRequest(transactionId, config.UnitId, startAddress, numberOfPoints);
await stream.WriteAsync(request, timeoutCts.Token);
ushort transactionId = NextTransactionId();
byte[] request = BuildReadCoilsRequest(transactionId, config.UnitId, startAddress, numberOfPoints);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] header = new byte[7];
await ReadExactlyAsync(stream, header, timeoutCts.Token);
byte[] header = new byte[7];
await ReadExactlyAsync(stream, header, timeoutCts.Token);
int remainingLength = ValidateReadResponseHeader(header, request);
byte[] pdu = new byte[remainingLength];
await ReadExactlyAsync(stream, pdu, timeoutCts.Token);
if (pdu[0] == 0x81)
{
byte exceptionCode = pdu.Length > 1 ? pdu[1] : (byte)0;
throw new InvalidOperationException($"PLC 读线圈异常,功能码 0x01异常码 0x{exceptionCode:X2}。");
}
if (pdu[0] != 0x01)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{pdu[0]:X2}。");
}
int expectedByteCount = (numberOfPoints + 7) / 8;
int byteCount = pdu[1];
if (byteCount != expectedByteCount || pdu.Length < 2 + byteCount)
{
throw new InvalidOperationException("PLC 读线圈响应字节数无效。");
}
bool[] coils = new bool[numberOfPoints];
for (int i = 0; i < coils.Length; i++)
{
coils[i] = (pdu[2 + i / 8] & (1 << (i % 8))) != 0;
}
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);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildWriteMultipleRegistersRequest(transactionId, config.UnitId, startAddress, values);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] response = new byte[12];
await ReadExactlyAsync(stream, response, timeoutCts.Token);
ValidateFixedResponseHeader(response, request);
if (response[7] == 0x90)
{
throw new InvalidOperationException($"PLC 写寄存器异常,功能码 0x10异常码 0x{response[8]:X2}。");
}
if (response[7] != 0x10)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{response[7]:X2}。");
}
ushort responseStart = BinaryPrimitives.ReadUInt16BigEndian(response.AsSpan(8, 2));
ushort responseCount = BinaryPrimitives.ReadUInt16BigEndian(response.AsSpan(10, 2));
if (responseStart != startAddress || responseCount != values.Length)
{
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 响应长度无效。");
}
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;
throw new InvalidOperationException($"PLC 读线圈异常,功能码 0x01异常码 0x{exceptionCode:X2}。");
}
if (pdu[0] != 0x01)
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{pdu[0]:X2}。");
}
int expectedByteCount = (numberOfPoints + 7) / 8;
int byteCount = pdu[1];
if (byteCount != expectedByteCount || pdu.Length < 2 + byteCount)
{
throw new InvalidOperationException("PLC 读线圈响应字节数无效。");
}
bool[] coils = new bool[numberOfPoints];
for (int i = 0; i < coils.Length; i++)
{
coils[i] = (pdu[2 + i / 8] & (1 << (i % 8))) != 0;
}
return coils;
return remainingLength;
}
private async Task WriteMultipleRegistersAsync(PlcConnectionConfig config, ushort startAddress, ushort[] values, CancellationToken cancellationToken)
private static void ValidateFixedResponseHeader(byte[] response, byte[] request)
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
ValidateMbapHeader(response, request);
if (BinaryPrimitives.ReadUInt16BigEndian(response.AsSpan(4, 2)) != 6)
{
throw new InvalidOperationException("PLC 响应长度无效。");
}
}
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
ushort transactionId = NextTransactionId();
byte[] request = BuildWriteMultipleRegistersRequest(transactionId, config.UnitId, startAddress, values);
await stream.WriteAsync(request, timeoutCts.Token);
byte[] response = new byte[12];
await ReadExactlyAsync(stream, response, timeoutCts.Token);
if (response[0] != request[0] || response[1] != request[1])
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[7] == 0x90)
if (response[2] != 0 || response[3] != 0)
{
throw new InvalidOperationException($"PLC 写寄存器异常,功能码 0x10异常码 0x{response[8]:X2}。");
throw new InvalidOperationException("PLC 响应协议标识无效。");
}
if (response[7] != 0x10)
if (response[6] != request[6])
{
throw new InvalidOperationException($"PLC 响应功能码无效0x{response[7]:X2}。");
}
ushort responseStart = BinaryPrimitives.ReadUInt16BigEndian(response.AsSpan(8, 2));
ushort responseCount = BinaryPrimitives.ReadUInt16BigEndian(response.AsSpan(10, 2));
if (responseStart != startAddress || responseCount != values.Length)
{
throw new InvalidOperationException("PLC 写寄存器响应地址或数量不匹配。");
throw new InvalidOperationException("PLC 响应站号不匹配。");
}
}