605 lines
19 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|