236 lines
7.7 KiB
C#
236 lines
7.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 readonly SemaphoreSlim _ioLock = 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 RegistersToFloat(registers[0], registers[1]);
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (!float.IsFinite(value))
|
||
throw new ArgumentOutOfRangeException(nameof(value), "PLC浮点写入值不能是NaN或Infinity。");
|
||
|
||
ushort[] registers = FloatToRegisters(value);
|
||
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 _ioLock.WaitAsync();
|
||
try
|
||
{
|
||
await EnsureConnectedAsync();
|
||
|
||
if (_master == null)
|
||
throw new InvalidOperationException("PLC连接未初始化");
|
||
|
||
return await action(_master);
|
||
}
|
||
catch
|
||
{
|
||
CloseConnection();
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
_ioLock.Release();
|
||
}
|
||
}
|
||
|
||
private float RegistersToFloat(ushort firstRegister, ushort secondRegister)
|
||
{
|
||
return _config.FloatWordOrder == PlcFloatWordOrder.HighWordFirst
|
||
? WordsToFloat(firstRegister, secondRegister)
|
||
: WordsToFloat(secondRegister, firstRegister);
|
||
}
|
||
|
||
private static float WordsToFloat(ushort highWord, ushort lowWord)
|
||
{
|
||
byte[] bytes =
|
||
{
|
||
(byte)(lowWord & 0xFF),
|
||
(byte)(lowWord >> 8),
|
||
(byte)(highWord & 0xFF),
|
||
(byte)(highWord >> 8)
|
||
};
|
||
return BitConverter.ToSingle(bytes, 0);
|
||
}
|
||
|
||
private ushort[] FloatToRegisters(float value)
|
||
{
|
||
byte[] bytes = BitConverter.GetBytes(value);
|
||
ushort highWord = (ushort)((bytes[3] << 8) | bytes[2]);
|
||
ushort lowWord = (ushort)((bytes[1] << 8) | bytes[0]);
|
||
|
||
return _config.FloatWordOrder == PlcFloatWordOrder.HighWordFirst
|
||
? new[] { highWord, lowWord }
|
||
: new[] { lowWord, highWord };
|
||
}
|
||
|
||
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();
|
||
_ioLock.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;
|
||
}
|
||
}
|
||
}
|