2026-01-26 20:41:34 +08:00
|
|
|
|
using S7.Net; // 确保引用 S7.Net 库
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Text;
|
2026-01-26 11:38:02 +08:00
|
|
|
|
using System.Threading;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
|
|
namespace RIZO.Service.PLC
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public class PlcConntectHepler : IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
private Plc _plc;
|
|
|
|
|
|
private readonly string _ipAddress;
|
|
|
|
|
|
private readonly CpuType _cpuType;
|
|
|
|
|
|
private readonly short _rack;
|
|
|
|
|
|
private readonly short _slot;
|
|
|
|
|
|
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
|
|
public PlcConntectHepler(string ipAddress, CpuType cpuType = CpuType.S71200, short rack = 0, short slot = 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ipAddress = ipAddress;
|
|
|
|
|
|
_cpuType = cpuType;
|
|
|
|
|
|
_rack = rack;
|
|
|
|
|
|
_slot = slot;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task ConnectAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
int maxRetries = 3;
|
|
|
|
|
|
int retryDelayMs = 2000;
|
|
|
|
|
|
int attempt = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (attempt < maxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
attempt++;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
_plc = new Plc(_cpuType, _ipAddress, _rack, _slot);
|
|
|
|
|
|
await Task.Run(() => _plc.Open(), cancellationToken); // 在线程池中执行同步Open
|
|
|
|
|
|
Console.WriteLine($"成功连接到PLC: {_ipAddress} (尝试{attempt}/{maxRetries})");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"连接尝试 {attempt}/{maxRetries} 失败: {ex.Message}");
|
|
|
|
|
|
|
|
|
|
|
|
if (attempt < maxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"{retryDelayMs / 1000}秒后重试...");
|
|
|
|
|
|
await Task.Delay(retryDelayMs, cancellationToken);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine("无法建立PLC连接,请检查网络和设备状态");
|
|
|
|
|
|
_plc?.Close();
|
|
|
|
|
|
_plc = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-26 20:41:34 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 读取指定类型数据
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T"></typeparam>
|
|
|
|
|
|
/// <param name="address"></param>
|
|
|
|
|
|
/// <param name="cancellationToken"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2026-01-26 11:38:02 +08:00
|
|
|
|
|
2026-01-26 20:41:34 +08:00
|
|
|
|
public async Task<T> ReadAsync<T>(string address, CancellationToken cancellationToken = default)
|
2026-01-26 11:38:02 +08:00
|
|
|
|
{
|
|
|
|
|
|
await _semaphore.WaitAsync(cancellationToken);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
await ConnectAsync(cancellationToken);
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
throw new InvalidOperationException("PLC未连接");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:41:34 +08:00
|
|
|
|
var result = await Task.Run(() => _plc.Read(address), cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
// 根据泛型类型转换结果
|
|
|
|
|
|
return ConvertResult<T>(result);
|
2026-01-26 11:38:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"Read error: {ex.Message}");
|
2026-01-26 20:41:34 +08:00
|
|
|
|
return default(T);
|
2026-01-26 11:38:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_semaphore.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:41:34 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 写入指定类型数据
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="address"></param>
|
|
|
|
|
|
/// <param name="value"></param>
|
|
|
|
|
|
/// <param name="cancellationToken"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2026-01-26 11:38:02 +08:00
|
|
|
|
public async Task WriteAsync(string address, object value, CancellationToken cancellationToken = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _semaphore.WaitAsync(cancellationToken);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
await ConnectAsync(cancellationToken);
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
throw new InvalidOperationException("PLC未连接");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await Task.Run(() => _plc.Write(address, value), cancellationToken);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"Write error: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_semaphore.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:41:34 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private T ConvertResult<T>(object result)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (result == null) return default(T);
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof(T).IsAssignableFrom(result.GetType()))
|
|
|
|
|
|
{
|
|
|
|
|
|
return (T)result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return typeof(T) switch
|
|
|
|
|
|
{
|
|
|
|
|
|
Type t when t == typeof(bool) => (T)(object)Convert.ToBoolean(result),
|
|
|
|
|
|
Type t when t == typeof(short) => (T)(object)Convert.ToInt16(result),
|
|
|
|
|
|
Type t when t == typeof(int) => (T)(object)Convert.ToInt32(result),
|
|
|
|
|
|
Type t when t == typeof(float) => (T)(object)Convert.ToSingle(result),
|
|
|
|
|
|
Type t when t == typeof(double) => (T)(object)Convert.ToDouble(result),
|
|
|
|
|
|
Type t when t == typeof(string) => (T)(object)result.ToString(),
|
|
|
|
|
|
_ => throw new InvalidCastException($"无法将类型 {result.GetType()} 转换为 {typeof(T)}")
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从PLC读取字符串(主要方法)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="address">地址字符串(如:"DB1001.DBB1000")</param>
|
|
|
|
|
|
/// <returns>读取到的字符串</returns>
|
|
|
|
|
|
public async Task<string> ReadStringAsync(string address)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await ReadStringAsync(address, CancellationToken.None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从PLC读取字符串(带取消令牌)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="address">地址字符串(如:"DB1001.DBB1000")</param>
|
|
|
|
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|
|
|
|
|
/// <returns>读取到的字符串</returns>
|
|
|
|
|
|
public async Task<string> ReadStringAsync(string address, CancellationToken cancellationToken = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _semaphore.WaitAsync(cancellationToken);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
await ConnectAsync(cancellationToken);
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
throw new InvalidOperationException("PLC未连接");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析地址
|
|
|
|
|
|
if (!ParseAddress(address, out int dbNumber, out int startByte))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException($"无效的地址格式: {address}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 读取第一个字节(字符串长度)
|
|
|
|
|
|
byte stringLength = await Task.Run(() =>
|
|
|
|
|
|
(byte)_plc.Read(DataType.DataBlock, dbNumber, startByte, VarType.Byte, 1), cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果长度为0,直接返回空字符串
|
|
|
|
|
|
if (stringLength == 0)
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
// 读取字符串内容(从startByte+1开始,读取stringLength个字节)
|
|
|
|
|
|
byte[] stringBytes = await Task.Run(() =>
|
|
|
|
|
|
(byte[])_plc.Read(DataType.DataBlock, dbNumber, startByte + 1, VarType.Byte, (int)stringLength), cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
// 将字节数组转换为ASCII字符串
|
|
|
|
|
|
return Encoding.ASCII.GetString(stringBytes);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"ReadString error: {ex.Message}");
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_semaphore.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 向PLC写入字符串
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="address">地址字符串(如:"DB1001.DBB1000")</param>
|
|
|
|
|
|
/// <param name="value">要写入的字符串</param>
|
|
|
|
|
|
/// <returns>是否写入成功</returns>
|
|
|
|
|
|
public async Task<bool> WriteStringAsync(string address, string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await WriteStringAsync(address, value, CancellationToken.None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 向PLC写入字符串(带取消令牌)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="address">地址字符串(如:"DB1001.DBB1000")</param>
|
|
|
|
|
|
/// <param name="value">要写入的字符串</param>
|
|
|
|
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|
|
|
|
|
/// <returns>是否写入成功</returns>
|
|
|
|
|
|
public async Task<bool> WriteStringAsync(string address, string value, CancellationToken cancellationToken = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _semaphore.WaitAsync(cancellationToken);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
await ConnectAsync(cancellationToken);
|
|
|
|
|
|
if (_plc == null || !_plc.IsConnected)
|
|
|
|
|
|
throw new InvalidOperationException("PLC未连接");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (value == null)
|
|
|
|
|
|
value = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
// 限制字符串长度(S7字符串最大254字符)
|
|
|
|
|
|
if (value.Length > 254)
|
|
|
|
|
|
{
|
|
|
|
|
|
value = value.Substring(0, 254);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 准备写入的数据
|
|
|
|
|
|
List<byte> writeData = new List<byte>();
|
|
|
|
|
|
|
|
|
|
|
|
// 第一个字节:字符串长度
|
|
|
|
|
|
writeData.Add((byte)value.Length);
|
|
|
|
|
|
|
|
|
|
|
|
// 后续字节:字符串内容(ASCII编码)
|
|
|
|
|
|
byte[] contentBytes = Encoding.ASCII.GetBytes(value);
|
|
|
|
|
|
writeData.AddRange(contentBytes);
|
|
|
|
|
|
|
|
|
|
|
|
// 解析地址
|
|
|
|
|
|
if (!ParseAddress(address, out int dbNumber, out int startByte))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException($"无效的地址格式: {address}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写入数据
|
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
|
_plc.WriteBytes(DataType.DataBlock, dbNumber, startByte, writeData.ToArray()), cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"WriteString error: {ex.Message}");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_semaphore.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析PLC地址字符串,提取DB号和起始字节
|
|
|
|
|
|
/// 支持格式:DB1001.DBB1000, DB1001.DBW1000, DB1001.DBD1000 等
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private bool ParseAddress(string address, out int dbNumber, out int startByte)
|
|
|
|
|
|
{
|
|
|
|
|
|
dbNumber = 0;
|
|
|
|
|
|
startByte = 0;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 移除空格并转为大写
|
|
|
|
|
|
address = address.Replace(" ", "").ToUpper();
|
|
|
|
|
|
|
|
|
|
|
|
// 分割DB部分和数据部分
|
|
|
|
|
|
string[] parts = address.Split('.');
|
|
|
|
|
|
if (parts.Length < 2) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 解析DB号
|
|
|
|
|
|
string dbPart = parts[0];
|
|
|
|
|
|
if (!dbPart.StartsWith("DB") || !int.TryParse(dbPart.Substring(2), out dbNumber))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 解析起始字节
|
|
|
|
|
|
string dataPart = parts[1];
|
|
|
|
|
|
|
|
|
|
|
|
// 提取偏移量数字(去掉DBB/DBW/DBD前缀)
|
|
|
|
|
|
string offsetStr = "";
|
|
|
|
|
|
if (dataPart.StartsWith("DBB"))
|
|
|
|
|
|
offsetStr = dataPart.Substring(3);
|
|
|
|
|
|
else if (dataPart.StartsWith("DBW"))
|
|
|
|
|
|
offsetStr = dataPart.Substring(3);
|
|
|
|
|
|
else if (dataPart.StartsWith("DBD"))
|
|
|
|
|
|
offsetStr = dataPart.Substring(3);
|
|
|
|
|
|
else
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (!int.TryParse(offsetStr, out startByte))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 11:38:02 +08:00
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
|
|
|
|
|
Dispose(true);
|
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_disposed) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (disposing)
|
|
|
|
|
|
{
|
|
|
|
|
|
_semaphore.Dispose();
|
|
|
|
|
|
_plc?.Close();
|
|
|
|
|
|
(_plc as IDisposable)?.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
_disposed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|