Files

236 lines
7.7 KiB
C#
Raw Permalink Normal View History

2026-05-16 17:47:46 +08:00
using Modbus.Device;
2026-05-05 15:31:24 +08:00
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using TabletTester2025.Models;
namespace TabletTester2025.Services
{
2026-05-16 17:47:46 +08:00
public class ModbusTcpPlcService : IPlcService, IDisposable
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
private const int ConnectTimeoutMs = 3000;
private const int RetryDelayMs = 1000;
private const int DefaultRetryCount = 3;
2026-05-05 15:31:24 +08:00
private readonly PlcConfiguration _config;
2026-05-16 17:47:46 +08:00
private readonly SemaphoreSlim _connectLock = new(1, 1);
2026-05-19 18:22:00 +08:00
private readonly SemaphoreSlim _ioLock = new(1, 1);
2026-05-16 17:47:46 +08:00
private TcpClient? _tcpClient;
private IModbusMaster? _master;
2026-05-05 15:31:24 +08:00
public ModbusTcpPlcService(PlcConfiguration config)
{
_config = config;
}
2026-05-16 17:47:46 +08:00
public Task ConnectAsync() => EnsureConnectedAsync();
2026-05-05 15:31:24 +08:00
2026-05-18 14:06:04 +08:00
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;
}
}
2026-05-16 17:47:46 +08:00
public async Task EnsureConnectedAsync(int retryCount = DefaultRetryCount)
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
if (HasOpenConnection)
2026-05-05 15:31:24 +08:00
return;
2026-05-16 17:47:46 +08:00
await _connectLock.WaitAsync();
try
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
if (HasOpenConnection)
2026-05-05 15:31:24 +08:00
return;
2026-05-16 17:47:46 +08:00
Exception? lastError = null;
for (int attempt = 1; attempt <= retryCount; attempt++)
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
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);
}
2026-05-05 15:31:24 +08:00
}
2026-05-16 17:47:46 +08:00
throw new InvalidOperationException($"无法连接到 PLC ({_config.IpAddress}:{_config.Port})", lastError);
}
finally
{
_connectLock.Release();
2026-05-05 15:31:24 +08:00
}
}
2026-05-16 17:47:46 +08:00
2026-05-05 15:31:24 +08:00
public async Task<float> ReadFloatAsync(ushort startAddress)
{
var registers = await ReadHoldingRegistersAsync(startAddress, 2);
2026-05-19 16:55:00 +08:00
return RegistersToFloat(registers[0], registers[1]);
2026-05-05 15:31:24 +08:00
}
2026-05-16 17:47:46 +08:00
2026-05-15 11:13:06 +08:00
public async Task<int> ReadIntAsync(ushort startAddress)
{
var registers = await ReadHoldingRegistersAsync(startAddress, 1);
return registers[0];
}
2026-05-05 15:31:24 +08:00
2026-05-16 17:47:46 +08:00
public Task WriteCoilAsync(ushort coilAddress, bool value)
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
return ExecuteAsync(master => master.WriteSingleCoilAsync(_config.SlaveId, coilAddress, value));
2026-05-05 15:31:24 +08:00
}
public async Task<bool> ReadCoilAsync(ushort coilAddress)
{
2026-05-16 17:47:46 +08:00
bool[] result = await ExecuteAsync(master => master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1));
2026-05-05 15:31:24 +08:00
return result[0];
}
2026-05-16 17:47:46 +08:00
public Task WriteRegisterAsync(ushort registerAddress, ushort value)
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
return ExecuteAsync(master => master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value));
2026-05-05 15:31:24 +08:00
}
2026-05-16 17:47:46 +08:00
public Task WriteFloatAsync(ushort startAddress, float value)
2026-05-16 16:58:57 +08:00
{
2026-05-19 16:55:00 +08:00
if (!float.IsFinite(value))
throw new ArgumentOutOfRangeException(nameof(value), "PLC浮点写入值不能是NaN或Infinity。");
2026-05-16 17:47:46 +08:00
2026-05-19 16:55:00 +08:00
ushort[] registers = FloatToRegisters(value);
2026-05-16 17:47:46 +08:00
return ExecuteAsync(master => master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers));
2026-05-16 16:58:57 +08:00
}
2026-05-16 17:47:46 +08:00
public Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count)
2026-05-05 15:31:24 +08:00
{
2026-05-16 17:47:46 +08:00
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;
});
2026-05-05 15:31:24 +08:00
}
2026-05-16 17:47:46 +08:00
private async Task<T> ExecuteAsync<T>(Func<IModbusMaster, Task<T>> action)
{
2026-05-19 18:22:00 +08:00
await _ioLock.WaitAsync();
2026-05-16 17:47:46 +08:00
try
{
2026-05-19 18:22:00 +08:00
await EnsureConnectedAsync();
2026-05-16 17:47:46 +08:00
if (_master == null)
throw new InvalidOperationException("PLC连接未初始化");
return await action(_master);
}
catch
{
CloseConnection();
throw;
}
2026-05-19 18:22:00 +08:00
finally
{
_ioLock.Release();
}
2026-05-16 17:47:46 +08:00
}
2026-05-05 15:31:24 +08:00
2026-05-19 18:22:00 +08:00
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)
2026-05-05 15:31:24 +08:00
{
2026-05-19 16:55:00 +08:00
byte[] bytes =
{
(byte)(lowWord & 0xFF),
(byte)(lowWord >> 8),
(byte)(highWord & 0xFF),
(byte)(highWord >> 8)
};
2026-05-05 15:31:24 +08:00
return BitConverter.ToSingle(bytes, 0);
}
2026-05-19 18:22:00 +08:00
private ushort[] FloatToRegisters(float value)
2026-05-19 16:55:00 +08:00
{
byte[] bytes = BitConverter.GetBytes(value);
2026-05-19 18:22:00 +08:00
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 };
2026-05-19 16:55:00 +08:00
}
2026-05-16 17:47:46 +08:00
private void CloseConnection()
{
try { _master?.Dispose(); } catch { }
try { _tcpClient?.Close(); } catch { }
try { _tcpClient?.Dispose(); } catch { }
_master = null;
_tcpClient = null;
}
2026-05-05 15:31:24 +08:00
public void Dispose()
{
2026-05-16 17:47:46 +08:00
CloseConnection();
_connectLock.Dispose();
2026-05-19 18:22:00 +08:00
_ioLock.Dispose();
2026-05-05 15:31:24 +08:00
}
}
public static class TaskExtensions
{
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
2026-05-16 17:47:46 +08:00
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s!).TrySetResult(true), tcs))
2026-05-05 15:31:24 +08:00
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
2026-05-16 17:47:46 +08:00
2026-05-05 15:31:24 +08:00
await task;
}
}
2026-05-16 16:58:57 +08:00
}