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

271 lines
9.0 KiB
C#
Raw 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 async Task<int> ReadInt32Async(ushort startAddress)
{
var registers = await ReadHoldingRegistersAsync(startAddress, 2);
return RegistersToInt32(registers[0], registers[1]);
}
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 WriteInt32Async(ushort startAddress, int value)
{
ushort[] registers = Int32ToRegisters(value);
return ExecuteAsync(master => master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers));
}
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 int RegistersToInt32(ushort firstRegister, ushort secondRegister)
{
return _config.FloatWordOrder == PlcFloatWordOrder.HighWordFirst
? WordsToInt32(firstRegister, secondRegister)
: WordsToInt32(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 static int WordsToInt32(ushort highWord, ushort lowWord)
{
return (int)(((uint)highWord << 16) | lowWord);
}
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 ushort[] Int32ToRegisters(int value)
{
uint raw = unchecked((uint)value);
ushort highWord = (ushort)(raw >> 16);
ushort lowWord = (ushort)(raw & 0xFFFF);
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;
}
}
}