Files
ConeCalorimeter/ConeCalorimeter/Services/TcpDeviceConnectionService.cs
2026-05-21 15:13:17 +08:00

605 lines
19 KiB
C#

using System.Diagnostics;
using System.Buffers.Binary;
using System.IO;
using System.Net.Sockets;
namespace ConeCalorimeter.Services;
public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
{
private const byte ReadCoilsFunction = 0x01;
private const byte ReadHoldingRegistersFunction = 0x03;
private const byte WriteSingleCoilFunction = 0x05;
private const byte WriteSingleRegisterFunction = 0x06;
private const byte WriteMultipleRegistersFunction = 0x10;
private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(3);
private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan ReadWriteTimeout = TimeSpan.FromSeconds(2);
private readonly object _syncRoot = new();
private readonly TcpDeviceConnectionOptions _options;
private CancellationTokenSource? _connectionLoopCancellation;
private Task? _connectionLoopTask;
private TcpClient? _client;
private ushort _transactionId;
private bool _isConnected;
private string _statusText;
public TcpDeviceConnectionService()
: this(TcpDeviceConnectionOptions.Default)
{
}
public TcpDeviceConnectionService(TcpDeviceConnectionOptions options)
{
_options = options;
_statusText = $"未连接设备 {_options.Host}:{_options.Port}";
}
public bool IsConnected
{
get
{
lock (_syncRoot)
{
return _isConnected;
}
}
}
public string Endpoint => $"{_options.Host}:{_options.Port}";
public string StatusText
{
get
{
lock (_syncRoot)
{
return _statusText;
}
}
}
public event EventHandler? ConnectionStatusChanged;
public Task StartAsync()
{
lock (_syncRoot)
{
if (_connectionLoopTask is not null)
{
return Task.CompletedTask;
}
_connectionLoopCancellation = new CancellationTokenSource();
_connectionLoopTask = RunConnectionLoopAsync(_connectionLoopCancellation.Token);
}
return Task.CompletedTask;
}
public async Task StopAsync()
{
Task? loopTask;
CancellationTokenSource? cancellation;
lock (_syncRoot)
{
loopTask = _connectionLoopTask;
cancellation = _connectionLoopCancellation;
_connectionLoopTask = null;
_connectionLoopCancellation = null;
}
if (cancellation is not null)
{
await cancellation.CancelAsync();
}
CloseCurrentClient();
SetConnectionState(false, $"未连接设备 {_options.Host}:{_options.Port}");
if (loopTask is not null)
{
try
{
await loopTask;
}
catch (OperationCanceledException)
{
}
}
cancellation?.Dispose();
Debug.WriteLine("TCP device connection service stopped.");
}
public async ValueTask DisposeAsync()
{
await StopAsync();
}
public bool TryReadFloat(ushort registerAddress, out double value)
{
value = double.NaN;
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
value = ReadFloat(_client, registerAddress);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryReadFloatValues(ushort registerAddress, out ModbusFloatReadResult result)
{
result = default;
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
result = ReadFloatValues(_client, registerAddress);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device register {registerAddress} float values read failed: {ex.Message}");
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryReadInt16(ushort registerAddress, out int value)
{
value = 0;
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
value = ReadInt16(_client, registerAddress);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryWriteInt16(ushort registerAddress, short value)
{
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
WriteInt16(_client, registerAddress, value);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device register {registerAddress} write failed: {ex.Message}");
SetConnectionState(false, $"写入寄存器 {registerAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryWriteFloat(ushort registerAddress, float value)
{
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
WriteFloat(_client, registerAddress, value);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device register {registerAddress} float write failed: {ex.Message}");
SetConnectionState(false, $"写入寄存器 {registerAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryReadCoil(ushort coilAddress, out bool value)
{
value = false;
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
value = ReadCoil(_client, coilAddress);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device coil {coilAddress} read failed: {ex.Message}");
SetConnectionState(false, $"读取线圈 {coilAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryWriteCoil(ushort coilAddress, bool value)
{
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
WriteCoil(_client, coilAddress, value);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device coil {coilAddress} write failed: {ex.Message}");
SetConnectionState(false, $"写入线圈 {coilAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
private async Task RunConnectionLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await ConnectAsync(cancellationToken);
await MonitorConnectionAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
Debug.WriteLine($"TCP device connection failed: {ex.Message}");
SetConnectionState(false, $"连接设备失败:{ex.Message}");
}
finally
{
CloseCurrentClient();
}
try
{
await Task.Delay(RetryDelay, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
break;
}
}
}
private async Task ConnectAsync(CancellationToken cancellationToken)
{
SetConnectionState(false, $"正在连接设备 {_options.Host}:{_options.Port}...");
Debug.WriteLine($"Connecting TCP device at {_options.Host}:{_options.Port}.");
var client = new TcpClient();
var connectTask = client.ConnectAsync(_options.Host, _options.Port, cancellationToken).AsTask();
var completedTask = await Task.WhenAny(connectTask, Task.Delay(ConnectTimeout, cancellationToken));
if (!ReferenceEquals(completedTask, connectTask))
{
client.Dispose();
throw new TimeoutException($"TCP device connection timed out after {ConnectTimeout.TotalSeconds:0} seconds.");
}
await connectTask;
client.ReceiveTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
client.SendTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
lock (_syncRoot)
{
_client?.Dispose();
_client = client;
}
SetConnectionState(true, $"已连接设备 {_options.Host}:{_options.Port}");
Debug.WriteLine($"TCP device connected at {_options.Host}:{_options.Port}.");
}
private async Task MonitorConnectionAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
TcpClient? client;
lock (_syncRoot)
{
client = _client;
}
if (client is null || !IsTcpClientConnected(client))
{
Debug.WriteLine("TCP device connection lost.");
SetConnectionState(false, "设备连接已断开");
return;
}
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
private static bool IsTcpClientConnected(TcpClient client)
{
try
{
var socket = client.Client;
return socket.Connected && !(socket.Poll(0, SelectMode.SelectRead) && socket.Available == 0);
}
catch (SocketException)
{
return false;
}
catch (ObjectDisposedException)
{
return false;
}
}
private void CloseCurrentClient()
{
lock (_syncRoot)
{
CloseCurrentClientCore();
}
}
private void CloseCurrentClientCore()
{
var client = _client;
_client = null;
_isConnected = false;
client?.Dispose();
}
private double ReadFloat(TcpClient client, ushort registerAddress)
{
return ReadFloatValues(client, registerAddress).Abcd;
}
private ModbusFloatReadResult ReadFloatValues(TcpClient client, ushort registerAddress)
{
var pdu = ReadHoldingRegisters(client, registerAddress, 2);
if (pdu.Length != 6 || pdu[1] != 4)
{
throw new InvalidDataException("Invalid Modbus TCP float response.");
}
return new ModbusFloatReadResult(pdu[2], pdu[3], pdu[4], pdu[5]);
}
private int ReadInt16(TcpClient client, ushort registerAddress)
{
var pdu = ReadHoldingRegisters(client, registerAddress, 1);
if (pdu.Length != 4 || pdu[1] != 2)
{
throw new InvalidDataException("Invalid Modbus TCP int16 response.");
}
return BinaryPrimitives.ReadInt16BigEndian(pdu[2..4]);
}
private void WriteInt16(TcpClient client, ushort registerAddress, short value)
{
Span<byte> payload = stackalloc byte[4];
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
BinaryPrimitives.WriteInt16BigEndian(payload[2..4], value);
var pdu = SendModbusRequest(client, WriteSingleRegisterFunction, payload);
if (pdu.Length != 5
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != registerAddress
|| BinaryPrimitives.ReadInt16BigEndian(pdu[3..5]) != value)
{
throw new InvalidDataException("Invalid Modbus TCP register write response.");
}
}
private void WriteFloat(TcpClient client, ushort registerAddress, float value)
{
Span<byte> payload = stackalloc byte[9];
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], 2);
payload[4] = 4;
BinaryPrimitives.WriteInt32BigEndian(payload[5..9], BitConverter.SingleToInt32Bits(value));
var pdu = SendModbusRequest(client, WriteMultipleRegistersFunction, payload);
if (pdu.Length != 5
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != registerAddress
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[3..5]) != 2)
{
throw new InvalidDataException("Invalid Modbus TCP float write response.");
}
}
private bool ReadCoil(TcpClient client, ushort coilAddress)
{
Span<byte> payload = stackalloc byte[4];
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], coilAddress);
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], 1);
var pdu = SendModbusRequest(client, ReadCoilsFunction, payload);
if (pdu.Length != 3 || pdu[1] != 1)
{
throw new InvalidDataException("Invalid Modbus TCP coil response.");
}
return (pdu[2] & 0x01) == 0x01;
}
private void WriteCoil(TcpClient client, ushort coilAddress, bool value)
{
Span<byte> payload = stackalloc byte[4];
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], coilAddress);
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], value ? (ushort)0xFF00 : (ushort)0x0000);
var pdu = SendModbusRequest(client, WriteSingleCoilFunction, payload);
if (pdu.Length != 5
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != coilAddress
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[3..5]) != (value ? (ushort)0xFF00 : (ushort)0x0000))
{
throw new InvalidDataException("Invalid Modbus TCP coil write response.");
}
}
private byte[] ReadHoldingRegisters(TcpClient client, ushort registerAddress, ushort registerCount)
{
Span<byte> payload = stackalloc byte[4];
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], registerCount);
return SendModbusRequest(client, ReadHoldingRegistersFunction, payload);
}
private byte[] SendModbusRequest(TcpClient client, byte functionCode, ReadOnlySpan<byte> payload)
{
var transactionId = ++_transactionId;
var pduLength = 1 + payload.Length;
var request = new byte[7 + pduLength];
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(0, 2), transactionId);
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(2, 2), 0);
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(4, 2), (ushort)(1 + pduLength));
request[6] = _options.UnitId;
request[7] = functionCode;
payload.CopyTo(request.AsSpan(8));
var stream = client.GetStream();
stream.Write(request);
Span<byte> header = stackalloc byte[7];
ReadExactly(stream, header);
var responseTransactionId = BinaryPrimitives.ReadUInt16BigEndian(header[0..2]);
var protocolId = BinaryPrimitives.ReadUInt16BigEndian(header[2..4]);
var length = BinaryPrimitives.ReadUInt16BigEndian(header[4..6]);
var unitId = header[6];
if (responseTransactionId != transactionId || protocolId != 0 || unitId != _options.UnitId || length < 2)
{
throw new InvalidDataException("Invalid Modbus TCP response header.");
}
var pdu = new byte[length - 1];
ReadExactly(stream, pdu);
if (pdu[0] == (functionCode | 0x80))
{
throw new InvalidDataException($"Modbus exception code {pdu[1]}.");
}
if (pdu[0] != functionCode)
{
throw new InvalidDataException("Unexpected Modbus TCP function code.");
}
return pdu;
}
private static void ReadExactly(NetworkStream stream, Span<byte> buffer)
{
var totalRead = 0;
while (totalRead < buffer.Length)
{
var read = stream.Read(buffer[totalRead..]);
if (read == 0)
{
throw new IOException("TCP device closed the connection.");
}
totalRead += read;
}
}
private void SetConnectionState(bool isConnected, string statusText)
{
var changed = false;
lock (_syncRoot)
{
if (_isConnected != isConnected || _statusText != statusText)
{
_isConnected = isConnected;
_statusText = statusText;
changed = true;
}
}
if (changed)
{
ConnectionStatusChanged?.Invoke(this, EventArgs.Empty);
}
}
}