212 lines
6.7 KiB
C#
212 lines
6.7 KiB
C#
using Modbus.Device;
|
||
using System;
|
||
using System.Net.Sockets;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using TabletTester2025.Models;
|
||
|
||
namespace TabletTester2025.Services
|
||
{
|
||
public class ModbusTcpPlcService : IPlcService, IDisposable
|
||
{
|
||
private const int ConnectTimeoutMs = 3000;
|
||
private const int RetryDelayMs = 1000;
|
||
private const int DefaultRetryCount = 3;
|
||
|
||
private readonly PlcConfiguration _config;
|
||
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
||
private TcpClient? _tcpClient;
|
||
private IModbusMaster? _master;
|
||
|
||
public ModbusTcpPlcService(PlcConfiguration config)
|
||
{
|
||
_config = config;
|
||
}
|
||
|
||
public Task ConnectAsync() => EnsureConnectedAsync();
|
||
|
||
public async Task<bool> CheckConnectionAsync()
|
||
{
|
||
try
|
||
{
|
||
if (_config.HardnessCompleteCoil != 0)
|
||
await ReadCoilAsync(_config.HardnessCompleteCoil);
|
||
else
|
||
await ReadHoldingRegistersAsync(_config.HardnessPoSun, 1);
|
||
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
CloseConnection();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public async Task EnsureConnectedAsync(int retryCount = DefaultRetryCount)
|
||
{
|
||
if (HasOpenConnection)
|
||
return;
|
||
|
||
await _connectLock.WaitAsync();
|
||
try
|
||
{
|
||
if (HasOpenConnection)
|
||
return;
|
||
|
||
Exception? lastError = null;
|
||
for (int attempt = 1; attempt <= retryCount; attempt++)
|
||
{
|
||
try
|
||
{
|
||
CloseConnection();
|
||
|
||
var client = new TcpClient();
|
||
using var cts = new CancellationTokenSource(ConnectTimeoutMs);
|
||
await client.ConnectAsync(_config.IpAddress, _config.Port).WithCancellation(cts.Token);
|
||
|
||
var master = ModbusIpMaster.CreateIp(client);
|
||
master.Transport.ReadTimeout = 1000;
|
||
master.Transport.WriteTimeout = 1000;
|
||
|
||
_tcpClient = client;
|
||
_master = master;
|
||
return;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
lastError = ex;
|
||
CloseConnection();
|
||
System.Diagnostics.Debug.WriteLine($"PLC连接失败,第{attempt}/{retryCount}次:{ex.Message}");
|
||
|
||
if (attempt < retryCount)
|
||
await Task.Delay(RetryDelayMs);
|
||
}
|
||
}
|
||
|
||
throw new InvalidOperationException($"无法连接到 PLC ({_config.IpAddress}:{_config.Port})", lastError);
|
||
}
|
||
finally
|
||
{
|
||
_connectLock.Release();
|
||
}
|
||
}
|
||
|
||
public async Task<float> ReadFloatAsync(ushort startAddress)
|
||
{
|
||
var registers = await ReadHoldingRegistersAsync(startAddress, 2);
|
||
return UshortToFloat(registers[1], registers[0]);
|
||
}
|
||
|
||
public async Task<int> ReadIntAsync(ushort startAddress)
|
||
{
|
||
var registers = await ReadHoldingRegistersAsync(startAddress, 1);
|
||
return registers[0];
|
||
}
|
||
|
||
public Task WriteCoilAsync(ushort coilAddress, bool value)
|
||
{
|
||
return ExecuteAsync(master => master.WriteSingleCoilAsync(_config.SlaveId, coilAddress, value));
|
||
}
|
||
|
||
public async Task<bool> ReadCoilAsync(ushort coilAddress)
|
||
{
|
||
bool[] result = await ExecuteAsync(master => master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1));
|
||
return result[0];
|
||
}
|
||
|
||
public Task WriteRegisterAsync(ushort registerAddress, ushort value)
|
||
{
|
||
return ExecuteAsync(master => master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value));
|
||
}
|
||
|
||
public Task WriteFloatAsync(ushort startAddress, float value)
|
||
{
|
||
byte[] bytes = BitConverter.GetBytes(value);
|
||
ushort[] registers =
|
||
{
|
||
(ushort)((bytes[2] << 8) | bytes[3]),
|
||
(ushort)((bytes[0] << 8) | bytes[1])
|
||
};
|
||
|
||
return ExecuteAsync(master => master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers));
|
||
}
|
||
|
||
public Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count)
|
||
{
|
||
return ExecuteAsync(master => master.ReadHoldingRegistersAsync(_config.SlaveId, startAddress, count));
|
||
}
|
||
|
||
public bool IsConnected => HasOpenConnection;
|
||
|
||
private bool HasOpenConnection => _tcpClient?.Connected == true && _master != null;
|
||
|
||
private async Task ExecuteAsync(Func<IModbusMaster, Task> action)
|
||
{
|
||
await ExecuteAsync(async master =>
|
||
{
|
||
await action(master);
|
||
return true;
|
||
});
|
||
}
|
||
|
||
private async Task<T> ExecuteAsync<T>(Func<IModbusMaster, Task<T>> action)
|
||
{
|
||
await EnsureConnectedAsync();
|
||
|
||
try
|
||
{
|
||
if (_master == null)
|
||
throw new InvalidOperationException("PLC连接未初始化");
|
||
|
||
return await action(_master);
|
||
}
|
||
catch
|
||
{
|
||
CloseConnection();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private static float UshortToFloat(ushort high, ushort low)
|
||
{
|
||
byte[] bytes = new byte[4];
|
||
bytes[0] = (byte)(high >> 8);
|
||
bytes[1] = (byte)(high & 0xFF);
|
||
bytes[2] = (byte)(low >> 8);
|
||
bytes[3] = (byte)(low & 0xFF);
|
||
return BitConverter.ToSingle(bytes, 0);
|
||
}
|
||
|
||
private void CloseConnection()
|
||
{
|
||
try { _master?.Dispose(); } catch { }
|
||
try { _tcpClient?.Close(); } catch { }
|
||
try { _tcpClient?.Dispose(); } catch { }
|
||
_master = null;
|
||
_tcpClient = null;
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
CloseConnection();
|
||
_connectLock.Dispose();
|
||
}
|
||
}
|
||
|
||
public static class TaskExtensions
|
||
{
|
||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||
{
|
||
var tcs = new TaskCompletionSource<bool>();
|
||
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s!).TrySetResult(true), tcs))
|
||
{
|
||
if (task != await Task.WhenAny(task, tcs.Task))
|
||
throw new OperationCanceledException(cancellationToken);
|
||
}
|
||
|
||
await task;
|
||
}
|
||
}
|
||
}
|