Files
CSI-Z420-Tablet-Multi-Funct…/Services/ModbusTcpPlcService.cs
GukSang.Jin 43893e5da6 更新2026
2026-05-20 11:38:23 +08:00

236 lines
7.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}