A:优化PLC数据管理

This commit is contained in:
jimmy committed 2025-12-09 13:44:09 +08:00
1 parent 2f3eb935dd
commit bc09280d4e
16 files changed
+592 -123

No files matched your search

+38 -8
View File
@@ -7,6 +7,7 @@ using Plc.Models;
using Plc.Siemens.Options;
using S7.Net;
using System.Windows;
using Sharp.Wpf.Abstractions;
namespace Client.ViewModels;
@@ -14,6 +15,7 @@ public class PlcViewModel : ViewModelBase
{
private readonly IPlcService _plcService;
private readonly SiemensPlcOptions _siemensOptions;
private readonly IDispatcher _dispatcher;
// 连接配置
private PlcBrand _selectedBrand = PlcBrand.Siemens;
@@ -26,24 +28,29 @@ public class PlcViewModel : ViewModelBase
private string _readValue = "-";
private string _writeAddress = "DB1.DBW0";
private string _writeValue = "0";
// 监控
private string _monitorAddress = "DB1.DBW0";
public PlcViewModel(IPlcService plcService, SiemensPlcOptions siemensOptions)
public PlcViewModel(
IPlcService plcService,
SiemensPlcOptions siemensOptions,
IDispatcher dispatcher)
{
_plcService = plcService;
_siemensOptions = siemensOptions;
_dispatcher = dispatcher;
// 初始化界面值为当前配置值
_ipAddress = _siemensOptions.IpAddress;
_selectedCpuType = _siemensOptions.CpuType;
ConnectCommand = new AsyncCommand(ConnectAsync);
DisconnectCommand = new AsyncCommand(DisconnectAsync);
ReadCommand = new AsyncCommand(ReadAsync);
WriteCommand = new AsyncCommand(WriteAsync);
AddMonitorCommand = new RelayCommand(AddMonitor);
RemoveMonitorCommand = new RelayCommand<PlcTag>(RemoveMonitor, tag => tag != null);
}
#region Properties
@@ -124,6 +131,7 @@ public class PlcViewModel : ViewModelBase
public IAsyncCommand ReadCommand { get; }
public IAsyncCommand WriteCommand { get; }
public ICommand AddMonitorCommand { get; }
public ICommand RemoveMonitorCommand { get; }
#endregion
@@ -201,12 +209,34 @@ public class PlcViewModel : ViewModelBase
DataType = typeof(short),
OnChanged = (t, v) =>
{
// 注意:UI绑定更新需要在UI线程,这里简化处理,假设WPF做了同步或者是后台线程安全集合
t.Value = v;
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
_dispatcher.InvokeAsync(() =>
{
t.Value = v;
});
}
};
_plcService.Subscribe(tag);
MonitoredTags.Add(tag);
// 在UI线程添加到集合
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Add(tag);
});
}
private void RemoveMonitor(PlcTag? tag)
{
if (tag == null) return;
// 从PLC服务取消订阅
_plcService.Unsubscribe(tag.Id);
// 从UI集合移除
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Remove(tag);
});
}
}
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Plc.Abstractions;
using Plc.Services;
using Plc.Siemens;
@@ -8,17 +9,44 @@ namespace Plc.Siemens.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSiemensPlc(this IServiceCollection services, Action<SiemensPlcOptions> configureOptions)
/// <summary>
/// 添加西门子PLC服务到DI容器
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configureOptions">配置选项的委托</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddSiemensPlc(
this IServiceCollection services,
Action<SiemensPlcOptions> configureOptions)
{
var options = new SiemensPlcOptions();
configureOptions(options);
services.AddSingleton(options);
services.AddSingleton<IPlcDriver, SiemensDriver>();
services.AddSingleton<IPlcOptimizer, SiemensOptimizer>();
// 使用标准的 IOptions 模式
services.Configure(configureOptions);
// 同时注册 SiemensPlcOptions 单例,供需要直接访问的地方使用(如 PlcViewModel)
// 注意:这是一个共享实例,从 IOptions 获取值
services.AddSingleton(sp => sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value);
// 注册驱动程序(使用 IOptions)
services.AddSingleton<IPlcDriver>(sp =>
{
var options = sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value;
return new SiemensDriver(options);
});
// 注册优化器(使用 IOptions 中的 MaxGapBytes)
services.AddSingleton<IPlcOptimizer>(sp =>
{
var options = sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value;
var logger = sp.GetService<Microsoft.Extensions.Logging.ILogger<SiemensOptimizer>>();
return new SiemensOptimizer(options.MaxGapBytes, logger);
});
// 注册数据转换器
services.AddSingleton<IDataConverter, BigEndianConverter>();
// 注册PLC服务
services.AddSingleton<IPlcService, PlcMonitorService>();
return services;
}
}
@@ -1,3 +1,5 @@
using Plc.Abstractions;
namespace Plc.Siemens.Models;
public enum SiemensMemoryArea
@@ -10,13 +12,13 @@ public enum SiemensMemoryArea
Counter = 0x1C
}
public class SiemensAddress
public class SiemensAddress : IPlcAddress
{
public SiemensMemoryArea Area { get; set; }
public int DbNumber { get; set; } // Only for DataBlock
public int ByteOffset { get; set; }
public int BitOffset { get; set; }
// Helper to sort/group
public override string ToString()
{
@@ -1,18 +1,28 @@
using S7.Net;
using System.ComponentModel.DataAnnotations;
using System.Net;
namespace Plc.Siemens.Options;
public class SiemensPlcOptions
public class SiemensPlcOptions : IValidatableObject
{
[Required]
public string IpAddress { get; set; } = "127.0.0.1";
[Range(1, 65535)]
public int Port { get; set; } = 102;
[Range(0, 255)]
public short Rack { get; set; } = 0;
[Range(0, 255)]
public short Slot { get; set; } = 1;
/// <summary>
/// S7-1200/1500 通常支持 960,S7-300 通常支持 240。
/// 默认保守值设为 240。
/// </summary>
[Range(128, 960)]
public int PduSize { get; set; } = 240;
/// <summary>
@@ -20,4 +30,39 @@ public class SiemensPlcOptions
/// 默认为 S71200。
/// </summary>
public CpuType CpuType { get; set; } = CpuType.S71200;
/// <summary>
/// 优化器允许的最大间隔字节数。
/// 两个标签之间如果间隔小于此值,会合并为一次读取。
/// 默认为 20。
/// </summary>
[Range(0, 1000)]
public int MaxGapBytes { get; set; } = 20;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// 验证IP地址格式
if (!IPAddress.TryParse(IpAddress, out _))
{
yield return new ValidationResult(
$"Invalid IP address format: {IpAddress}",
new[] { nameof(IpAddress) });
}
// 验证PDU大小合理性
if (PduSize < 128 || PduSize > 960)
{
yield return new ValidationResult(
$"PDU size must be between 128 and 960, got: {PduSize}",
new[] { nameof(PduSize) });
}
// 验证MaxGapBytes的合理性
if (MaxGapBytes < 0 || MaxGapBytes > PduSize)
{
yield return new ValidationResult(
$"MaxGapBytes ({MaxGapBytes}) must be between 0 and PduSize ({PduSize})",
new[] { nameof(MaxGapBytes) });
}
}
}
+2
View File
@@ -12,6 +12,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
<PackageReference Include="S7netplus" Version="0.20.0" />
</ItemGroup>
+32 -17
View File
@@ -11,11 +11,10 @@ public class SiemensDriver : IPlcDriver
{
private readonly SiemensPlcOptions _options;
private S7.Net.Plc? _plc;
private readonly object _lock = new();
public SiemensDriver(SiemensPlcOptions options)
{
_options = options;
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public bool IsConnected => _plc?.IsConnected ?? false;
@@ -34,46 +33,56 @@ public class SiemensDriver : IPlcDriver
}
// Create new instance with latest options
_plc = new S7.Net.Plc(_options.CpuType, _options.IpAddress, _options.Port, _options.Rack, _options.Slot);
_plc = new S7.Net.Plc(
_options.CpuType,
_options.IpAddress,
_options.Port,
_options.Rack,
_options.Slot);
await _plc.OpenAsync(cancellationToken);
if (_plc.IsConnected)
return Result.Success();
return Result.Failure("Failed to connect to PLC (IsConnected is false).");
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
return Result.Failure($"Connection failed: {ex.Message}");
}
}
public async Task<Result> DisconnectAsync()
public Task<Result> DisconnectAsync()
{
try
{
_plc?.Close();
return await Task.FromResult(Result.Success());
return Task.FromResult(Result.Success());
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
return Task.FromResult(Result.Failure($"Disconnect failed: {ex.Message}"));
}
}
public async Task<Result<byte[]>> ReadBytesAsync(string address, ushort length, CancellationToken cancellationToken = default)
{
if (!IsConnected || _plc == null)
return Result.Failure<byte[]>("PLC 未连接。");
return Result.Failure<byte[]>("PLC is not connected");
try
{
var siemensAddr = SiemensAddressParser.Parse(address);
var dataType = MapToS7DataType(siemensAddr.Area);
var bytes = await _plc.ReadBytesAsync(dataType, siemensAddr.DbNumber, siemensAddr.ByteOffset, length, cancellationToken);
var bytes = await _plc.ReadBytesAsync(
dataType,
siemensAddr.DbNumber,
siemensAddr.ByteOffset,
length,
cancellationToken);
return Result.Success(bytes);
}
catch (Exception ex)
@@ -85,15 +94,20 @@ public class SiemensDriver : IPlcDriver
public async Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default)
{
if (!IsConnected || _plc == null)
return Result.Failure("PLC 未连接。");
return Result.Failure("PLC is not connected");
try
{
var siemensAddr = SiemensAddressParser.Parse(address);
var dataType = MapToS7DataType(siemensAddr.Area);
await _plc.WriteBytesAsync(dataType, siemensAddr.DbNumber, siemensAddr.ByteOffset, data, cancellationToken);
await _plc.WriteBytesAsync(
dataType,
siemensAddr.DbNumber,
siemensAddr.ByteOffset,
data,
cancellationToken);
return Result.Success();
}
catch (Exception ex)
@@ -112,12 +126,13 @@ public class SiemensDriver : IPlcDriver
SiemensMemoryArea.Outputs => DataType.Output,
SiemensMemoryArea.Timer => DataType.Timer,
SiemensMemoryArea.Counter => DataType.Counter,
_ => throw new ArgumentOutOfRangeException(nameof(area), area, null)
_ => throw new ArgumentOutOfRangeException(nameof(area), area, $"Unsupported memory area: {area}")
};
}
public void Dispose()
{
_plc?.Close();
_plc = null;
}
}
+34 -6
View File
@@ -3,13 +3,23 @@ using Plc.Models;
using Plc.Siemens.Models;
using Plc.Siemens.Utils;
using Plc.Utils;
using Microsoft.Extensions.Logging;
namespace Plc.Siemens;
public class SiemensOptimizer : IPlcOptimizer
{
// 允许变量之间合并为一次读取的最大间隔字节数
private const int MaxGapBytes = 20;
private const int DefaultMaxGapBytes = 20;
private readonly int _maxGapBytes;
private readonly ILogger<SiemensOptimizer>? _logger;
public SiemensOptimizer(int maxGapBytes = DefaultMaxGapBytes, ILogger<SiemensOptimizer>? logger = null)
{
_maxGapBytes = maxGapBytes;
_logger = logger;
}
public List<PlcReadBatch> Optimize(IEnumerable<PlcTag> tags, int maxPduSize)
{
@@ -24,16 +34,21 @@ public class SiemensOptimizer : IPlcOptimizer
try
{
tag.ParsedAddress = SiemensAddressParser.Parse(tag.Address);
_logger?.LogTrace("Parsed address {Address} for tag {TagId}", tag.Address, tag.Id);
}
catch
catch (Exception ex)
{
// 记录错误?暂时跳过。
_logger?.LogWarning(ex, "Failed to parse address {Address} for tag {TagId}, skipping",
tag.Address, tag.Id);
continue;
}
}
validTags.Add(tag);
}
_logger?.LogDebug("Optimizing {ValidCount} valid tags out of {TotalCount} tags",
validTags.Count, tags.Count());
// 2. 按区域和 DB 号分组(物理位置)
var groups = validTags.GroupBy(t =>
{
@@ -46,20 +61,26 @@ public class SiemensOptimizer : IPlcOptimizer
// 3. 按字节偏移量排序
var sortedTags = group.OrderBy(t => ((SiemensAddress)t.ParsedAddress!).ByteOffset).ToList();
_logger?.LogTrace("Processing group: Area={Area}, DB={DbNumber}, Tags={Count}",
group.Key.Area, group.Key.DbNumber, sortedTags.Count);
PlcReadBatch? currentBatch = null;
foreach (var tag in sortedTags)
{
var addr = (SiemensAddress)tag.ParsedAddress!;
int typeSize = TypeHelper.GetSize(tag.DataType);
int typeSize = tag.DataType == typeof(string)
? TypeHelper.GetSize(tag.DataType, tag.Length)
: TypeHelper.GetSize(tag.DataType);
int byteLength = typeSize * tag.Length; // 此标签的总字节数
// 如果是 bool,它可能在一个字节内,但我们在检查边界时假设它占用该字节
if (currentBatch == null)
{
currentBatch = CreateBatch(tag, addr, byteLength);
batches.Add(currentBatch);
_logger?.LogTrace("Created new batch starting at {Address}", tag.Address);
continue;
}
@@ -77,20 +98,27 @@ public class SiemensOptimizer : IPlcOptimizer
// 1. 标签在当前结束之后(或位置相同)开始(因为已排序,所以是肯定的)
// 2. 间隔足够小
// 3. 总大小适合 PDU
if (gap >= 0 && gap <= MaxGapBytes && newTotalLength <= maxPduSize)
if (gap >= 0 && gap <= _maxGapBytes && newTotalLength <= maxPduSize)
{
currentBatch.Tags.Add(tag);
currentBatch.TotalLength = newTotalLength;
_logger?.LogTrace("Merged tag {TagId} into batch (gap={Gap}, newLength={Length})",
tag.Id, gap, newTotalLength);
}
else
{
// 开启新批次
_logger?.LogTrace("Cannot merge tag {TagId} (gap={Gap}, newLength={Length}, maxPdu={MaxPdu}), creating new batch",
tag.Id, gap, newTotalLength, maxPduSize);
currentBatch = CreateBatch(tag, addr, byteLength);
batches.Add(currentBatch);
}
}
}
_logger?.LogDebug("Optimization complete: {TagCount} tags -> {BatchCount} batches",
validTags.Count, batches.Count);
return batches;
}
+18 -5
View File
@@ -2,9 +2,22 @@ namespace Plc.Abstractions;
public interface IDataConverter
{
// 将原始字节(从指定偏移量开始)转换为目标类型
object? Parse(byte[] buffer, int offset, Type type, int length);
// 将值转换为字节
byte[] ToBytes(object value, Type type, int length);
/// <summary>
/// 将原始字节(从指定偏移量开始)转换为目标类型
/// </summary>
/// <param name="buffer">字节缓冲区</param>
/// <param name="offset">起始偏移量</param>
/// <param name="type">目标类型</param>
/// <param name="length">数据长度(对字符串和数组有效)</param>
/// <param name="bitOffset">位偏移量(仅对bool类型有效)</param>
object? Parse(byte[] buffer, int offset, Type type, int length, int bitOffset = 0);
/// <summary>
/// 将值转换为字节
/// </summary>
/// <param name="value">要转换的值</param>
/// <param name="type">值的类型</param>
/// <param name="length">数据长度(对字符串和数组有效)</param>
/// <param name="bitOffset">位偏移量(仅对bool类型有效)</param>
byte[] ToBytes(object value, Type type, int length, int bitOffset = 0);
}
+22
View File
@@ -0,0 +1,22 @@
namespace Plc.Abstractions;
/// <summary>
/// 定义PLC地址的通用接口,用于不同品牌的地址解析结果
/// </summary>
public interface IPlcAddress
{
/// <summary>
/// 获取字节偏移量
/// </summary>
int ByteOffset { get; }
/// <summary>
/// 获取位偏移量(仅对bool类型有效)
/// </summary>
int BitOffset { get; }
/// <summary>
/// 获取地址的字符串表示
/// </summary>
string ToString();
}
+1 -1
View File
@@ -42,4 +42,4 @@ public interface IPlcDriver : IDisposable
/// <param name="address">起始地址字符串。</param>
/// <param name="data">要写入的字节。</param>
Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default);
}
}
+3 -2
View File
@@ -35,9 +35,10 @@ public class PlcReadBatch
/// 包含在此批量范围内的标签列表。
/// </summary>
public List<PlcTag> Tags { get; set; } = new();
/// <summary>
/// 驱动程序特定的起始地址上下文(避免重新解析)。
/// 使用 IPlcAddress 接口以支持类型安全访问。
/// </summary>
public object? ParsedStartAddress { get; set; }
public IPlcAddress? ParsedStartAddress { get; set; }
}
+4 -1
View File
@@ -1,3 +1,5 @@
using Plc.Abstractions;
namespace Plc.Models;
/// <summary>
@@ -39,6 +41,7 @@ public class PlcTag
/// <summary>
/// 内部使用:由驱动程序/优化器用于存储解析后的地址对象。
/// 这避免了在每个周期重新解析地址字符串。
/// 使用 IPlcAddress 接口以支持类型安全访问。
/// </summary>
public object? ParsedAddress { get; set; }
public IPlcAddress? ParsedAddress { get; set; }
}
+4
View File
@@ -10,4 +10,8 @@
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>
+133 -37
View File
@@ -5,54 +5,129 @@ namespace Plc.Services;
public class BigEndianConverter : IDataConverter
{
public object? Parse(byte[] buffer, int offset, Type type, int length)
public object? Parse(byte[] buffer, int offset, Type type, int length, int bitOffset = 0)
{
// 为简洁起见省略了边界检查,但在生产环境中至关重要
// 参数验证
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (offset < 0 || offset >= buffer.Length)
throw new ArgumentOutOfRangeException(nameof(offset), "Offset is out of buffer range");
// Bool类型处理(按位)
if (type == typeof(bool))
return buffer[offset] > 0; // 简单的检查,真实的位逻辑需要位索引
if (type == typeof(byte))
return buffer[offset];
if (type == typeof(short))
return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset));
if (type == typeof(ushort))
return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset));
if (type == typeof(int))
return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset));
if (type == typeof(uint))
return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset));
if (type == typeof(float))
return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)));
if (type == typeof(double))
return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(offset)));
// 简单处理字符串 (假设 ASCII/S7 String)
if (type == typeof(string))
{
// S7 String 前两个字节是 MaxLen 和 ActualLen,后面是字符
// 这里假设是纯字符数组或者需要特定处理
// 简单起见,按 ASCII 读取指定长度
return System.Text.Encoding.ASCII.GetString(buffer, offset, length);
if (offset >= buffer.Length)
throw new ArgumentException("Buffer too small for bool");
if (bitOffset < 0 || bitOffset > 7)
throw new ArgumentOutOfRangeException(nameof(bitOffset), "Bit offset must be 0-7");
byte b = buffer[offset];
return (b & (1 << bitOffset)) != 0;
}
return null;
// Byte类型
if (type == typeof(byte))
{
if (offset >= buffer.Length)
throw new ArgumentException("Buffer too small for byte");
return buffer[offset];
}
// Short类型
if (type == typeof(short))
{
if (offset + 2 > buffer.Length)
throw new ArgumentException("Buffer too small for short");
return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset));
}
// UShort类型
if (type == typeof(ushort))
{
if (offset + 2 > buffer.Length)
throw new ArgumentException("Buffer too small for ushort");
return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset));
}
// Int类型
if (type == typeof(int))
{
if (offset + 4 > buffer.Length)
throw new ArgumentException("Buffer too small for int");
return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset));
}
// UInt类型
if (type == typeof(uint))
{
if (offset + 4 > buffer.Length)
throw new ArgumentException("Buffer too small for uint");
return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset));
}
// Float类型
if (type == typeof(float))
{
if (offset + 4 > buffer.Length)
throw new ArgumentException("Buffer too small for float");
return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)));
}
// Double类型
if (type == typeof(double))
{
if (offset + 8 > buffer.Length)
throw new ArgumentException("Buffer too small for double");
return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(offset)));
}
// String类型(S7 String格式)
if (type == typeof(string))
{
// S7 String结构:
// 字节0: MaxLength
// 字节1: ActualLength
// 字节2+: 字符数据
if (offset + 2 > buffer.Length)
throw new ArgumentException("Buffer too small for S7 string header");
int maxLen = buffer[offset];
int actualLen = buffer[offset + 1];
if (actualLen > maxLen)
actualLen = maxLen; // 安全处理
if (offset + 2 + actualLen > buffer.Length)
throw new ArgumentException($"Buffer too small for S7 string data (needs {actualLen} bytes)");
return System.Text.Encoding.ASCII.GetString(buffer, offset + 2, actualLen);
}
throw new NotSupportedException($"Type {type.Name} is not supported by this converter");
}
public byte[] ToBytes(object value, Type type, int length)
public byte[] ToBytes(object value, Type type, int length, int bitOffset = 0)
{
if (type == typeof(bool))
return new[] { (bool)value ? (byte)1 : (byte)0 };
if (value == null)
throw new ArgumentNullException(nameof(value));
// Bool类型(按位)
if (type == typeof(bool))
{
if (bitOffset < 0 || bitOffset > 7)
throw new ArgumentOutOfRangeException(nameof(bitOffset), "Bit offset must be 0-7");
byte b = 0;
if ((bool)value)
b = (byte)(1 << bitOffset);
return new[] { b };
}
// Byte类型
if (type == typeof(byte))
return new[] { (byte)value };
// Short类型
if (type == typeof(short))
{
var bytes = new byte[2];
@@ -60,6 +135,7 @@ public class BigEndianConverter : IDataConverter
return bytes;
}
// UShort类型
if (type == typeof(ushort))
{
var bytes = new byte[2];
@@ -67,6 +143,7 @@ public class BigEndianConverter : IDataConverter
return bytes;
}
// Int类型
if (type == typeof(int))
{
var bytes = new byte[4];
@@ -74,6 +151,7 @@ public class BigEndianConverter : IDataConverter
return bytes;
}
// UInt类型
if (type == typeof(uint))
{
var bytes = new byte[4];
@@ -81,6 +159,7 @@ public class BigEndianConverter : IDataConverter
return bytes;
}
// Float类型
if (type == typeof(float))
{
var bytes = new byte[4];
@@ -89,6 +168,7 @@ public class BigEndianConverter : IDataConverter
return bytes;
}
// Double类型
if (type == typeof(double))
{
var bytes = new byte[8];
@@ -96,7 +176,23 @@ public class BigEndianConverter : IDataConverter
BinaryPrimitives.WriteInt64BigEndian(bytes, longVal);
return bytes;
}
return Array.Empty<byte>();
// String类型(S7 String格式)
if (type == typeof(string))
{
var str = (string)value;
var strBytes = System.Text.Encoding.ASCII.GetBytes(str);
var actualLen = Math.Min(strBytes.Length, length);
// S7 String结构
var result = new byte[2 + length];
result[0] = (byte)length; // MaxLength
result[1] = (byte)actualLen; // ActualLength
Array.Copy(strBytes, 0, result, 2, actualLen);
return result;
}
throw new NotSupportedException($"Type {type.Name} is not supported by this converter");
}
}
+179 -31
View File
@@ -3,6 +3,7 @@ using Plc.Abstractions;
using Plc.Models;
using Plc.Utils;
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
namespace Plc.Services;
@@ -11,91 +12,197 @@ public class PlcMonitorService : IPlcService, IDisposable
private readonly IPlcDriver _driver;
private readonly IPlcOptimizer _optimizer;
private readonly IDataConverter _converter;
private readonly ILogger<PlcMonitorService>? _logger;
private readonly ConcurrentDictionary<string, PlcTag> _subscriptions = new();
private Task? _monitorTask;
private CancellationTokenSource? _cts;
private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(200);
public PlcMonitorService(IPlcDriver driver, IPlcOptimizer optimizer, IDataConverter converter)
// 重连策略
private int _reconnectAttempts = 0;
private const int MaxReconnectAttempts = 10;
public PlcMonitorService(
IPlcDriver driver,
IPlcOptimizer optimizer,
IDataConverter converter,
ILogger<PlcMonitorService>? logger = null)
{
_driver = driver;
_optimizer = optimizer;
_converter = converter;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken = default)
{
if (_monitorTask != null) return;
if (_monitorTask != null)
{
_logger?.LogWarning("Monitor service is already running");
return;
}
_logger?.LogInformation("Starting PLC monitor service");
var connectResult = await _driver.ConnectAsync(cancellationToken);
if (connectResult.IsFailure)
{
_logger?.LogError("Failed to connect to PLC: {Error}", connectResult.ErrorMessage);
throw new InvalidOperationException($"Cannot start monitor service: {connectResult.ErrorMessage}");
}
await _driver.ConnectAsync(cancellationToken);
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_monitorTask = Task.Run(() => MonitorLoop(_cts.Token), _cts.Token);
_logger?.LogInformation("PLC monitor service started successfully");
}
public async Task StopAsync()
{
_logger?.LogInformation("Stopping PLC monitor service");
_cts?.Cancel();
if (_monitorTask != null)
{
try { await _monitorTask; } catch (OperationCanceledException) { }
try
{
await _monitorTask;
}
catch (OperationCanceledException)
{
_logger?.LogDebug("Monitor task cancelled successfully");
}
}
await _driver.DisconnectAsync();
_logger?.LogInformation("PLC monitor service stopped");
}
public void Subscribe(PlcTag tag)
{
_subscriptions.TryAdd(tag.Id, tag);
if (_subscriptions.TryAdd(tag.Id, tag))
{
_logger?.LogDebug("Subscribed to tag {TagId} at address {Address}", tag.Id, tag.Address);
}
else
{
_logger?.LogWarning("Tag {TagId} is already subscribed", tag.Id);
}
}
public void Unsubscribe(string tagId)
{
_subscriptions.TryRemove(tagId, out _);
if (_subscriptions.TryRemove(tagId, out _))
{
_logger?.LogDebug("Unsubscribed tag {TagId}", tagId);
}
else
{
_logger?.LogWarning("Failed to unsubscribe tag {TagId} - not found", tagId);
}
}
public async Task<Result<T>> ReadAsync<T>(string address)
{
_logger?.LogDebug("Reading from address {Address}", address);
// 临时读取(未优化)
int size = TypeHelper.GetSize(typeof(T));
var result = await _driver.ReadBytesAsync(address, (ushort)size);
if (result.IsFailure)
if (result.IsFailure)
{
_logger?.LogError("Failed to read from {Address}: {Error}", address, result.ErrorMessage);
return Result.Failure<T>(result.ErrorMessage ?? result.Exception?.Message ?? "Read failed");
}
if (result.Value is null)
{
_logger?.LogError("Read from {Address} returned null data", address);
return Result.Failure<T>("Read returned null data");
}
var val = _converter.Parse(result.Value, 0, typeof(T), 1);
if (val is null)
{
_logger?.LogError("Failed to convert data from {Address}", address);
return Result.Failure<T>("Conversion failed");
}
_logger?.LogDebug("Successfully read value from {Address}", address);
return Result.Success((T)val);
}
public async Task<Result> WriteAsync<T>(string address, T value)
{
if (value == null)
{
_logger?.LogWarning("Attempted to write null value to {Address}", address);
return Result.Failure("Cannot write null value");
}
_logger?.LogDebug("Writing to address {Address}", address);
int size = TypeHelper.GetSize(typeof(T));
var bytes = _converter.ToBytes(value, typeof(T), size);
if (bytes.Length == 0)
return Result.Failure("Conversion to bytes returned empty array. Type might not be supported.");
return await _driver.WriteBytesAsync(address, bytes);
if (bytes.Length == 0)
{
_logger?.LogError("Conversion to bytes failed for type {Type}", typeof(T).Name);
return Result.Failure("Conversion to bytes returned empty array. Type might not be supported.");
}
var result = await _driver.WriteBytesAsync(address, bytes);
if (result.IsFailure)
{
_logger?.LogError("Failed to write to {Address}: {Error}", address, result.ErrorMessage);
}
else
{
_logger?.LogDebug("Successfully wrote to {Address}", address);
}
return result;
}
private async Task MonitorLoop(CancellationToken token)
{
_logger?.LogInformation("Monitor loop started");
while (!token.IsCancellationRequested)
{
try
{
// 连接检查和重试逻辑
if (!_driver.IsConnected)
{
await _driver.ConnectAsync(token);
_logger?.LogWarning("PLC disconnected, attempting to reconnect (attempt {Attempt}/{Max})",
_reconnectAttempts + 1, MaxReconnectAttempts);
// 指数退避策略
var delay = Math.Min(1000 * Math.Pow(2, _reconnectAttempts), 30000);
await Task.Delay((int)delay, token);
var connectResult = await _driver.ConnectAsync(token);
if (connectResult.IsSuccess)
{
_logger?.LogInformation("Successfully reconnected to PLC");
_reconnectAttempts = 0;
}
else
{
_reconnectAttempts++;
_logger?.LogError("Reconnection failed: {Error}", connectResult.ErrorMessage);
if (_reconnectAttempts >= MaxReconnectAttempts)
{
_logger?.LogCritical("Max reconnection attempts reached, monitor loop will continue trying");
_reconnectAttempts = 0; // 重置以继续尝试
}
continue;
}
}
var currentTags = _subscriptions.Values.ToList();
@@ -107,6 +214,8 @@ public class PlcMonitorService : IPlcService, IDisposable
// 优化:计算分桶
var batches = _optimizer.Optimize(currentTags, _driver.MaxPduSize);
_logger?.LogTrace("Optimized {TagCount} tags into {BatchCount} batches",
currentTags.Count, batches.Count);
// 执行读取
foreach (var batch in batches)
@@ -114,55 +223,94 @@ public class PlcMonitorService : IPlcService, IDisposable
if (token.IsCancellationRequested) break;
var readResult = await _driver.ReadBytesAsync(batch.StartAddress, (ushort)batch.TotalLength, token);
if (readResult.IsSuccess && readResult.Value != null)
{
DistributeValues(batch, readResult.Value);
}
else
{
// 记录错误
_logger?.LogWarning("Batch read failed for address {Address}: {Error}",
batch.StartAddress, readResult.ErrorMessage);
}
}
}
catch (Exception)
catch (OperationCanceledException)
{
// 记录异常
_logger?.LogDebug("Monitor loop cancelled");
break;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error in monitor loop");
}
await Task.Delay(_refreshRate, token);
}
_logger?.LogInformation("Monitor loop stopped");
}
private void DistributeValues(PlcReadBatch batch, byte[] data)
{
dynamic batchStart = batch.ParsedStartAddress!;
if (batch.ParsedStartAddress == null)
{
_logger?.LogError("Batch missing parsed start address");
return;
}
var batchStart = batch.ParsedStartAddress;
int batchStartByte = batchStart.ByteOffset;
foreach (var tag in batch.Tags)
{
dynamic tagAddr = tag.ParsedAddress!;
if (tag.ParsedAddress == null)
{
_logger?.LogWarning("Tag {TagId} missing parsed address", tag.Id);
continue;
}
var tagAddr = tag.ParsedAddress;
int tagStartByte = tagAddr.ByteOffset;
int relativeOffset = tagStartByte - batchStartByte;
// 检查边界
int typeSize = TypeHelper.GetSize(tag.DataType);
int typeSize = tag.DataType == typeof(string)
? TypeHelper.GetSize(tag.DataType, tag.Length)
: TypeHelper.GetSize(tag.DataType);
if (relativeOffset + typeSize <= data.Length)
{
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length);
// 变更检测
if (!Equals(tag.Value, newVal))
try
{
tag.Value = newVal;
tag.OnChanged?.Invoke(tag, newVal);
// 传递位偏移量以支持bool类型
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length, tagAddr.BitOffset);
// 变更检测
if (!Equals(tag.Value, newVal))
{
tag.Value = newVal;
tag.OnChanged?.Invoke(tag, newVal);
_logger?.LogTrace("Tag {TagId} value changed to {Value}", tag.Id, newVal);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to parse value for tag {TagId} at address {Address}",
tag.Id, tag.Address);
}
}
else
{
_logger?.LogWarning("Tag {TagId} offset {Offset} + size {Size} exceeds batch data length {Length}",
tag.Id, relativeOffset, typeSize, data.Length);
}
}
}
public void Dispose()
{
_logger?.LogDebug("Disposing PLC monitor service");
_cts?.Cancel();
_driver.Dispose();
}
+35 -3
View File
@@ -2,14 +2,46 @@ namespace Plc.Utils;
public static class TypeHelper
{
/// <summary>
/// 获取PLC数据类型的字节大小
/// </summary>
/// <param name="type">数据类型</param>
/// <returns>字节大小</returns>
/// <exception cref="ArgumentException">当类型不支持或需要明确长度时抛出</exception>
public static int GetSize(Type type)
{
if (type == typeof(bool)) return 1; // S7 usually treats bool as bit, but allocation often aligns to bytes or we need spec handling
if (type == typeof(bool)) return 1; // S7中bool按字节对齐
if (type == typeof(byte)) return 1;
if (type == typeof(short) || type == typeof(ushort)) return 2;
if (type == typeof(int) || type == typeof(uint) || type == typeof(float)) return 4;
if (type == typeof(long) || type == typeof(ulong) || type == typeof(double)) return 8;
return 1; // Default or throw
// 字符串类型需要明确指定长度
if (type == typeof(string))
{
throw new ArgumentException(
"String type requires explicit length specification. Use the overload with length parameter or specify length in PlcTag.",
nameof(type));
}
throw new NotSupportedException($"Type {type.Name} is not supported. Supported types: bool, byte, short, ushort, int, uint, float, long, ulong, double, string.");
}
/// <summary>
/// 获取PLC数据类型的字节大小(支持字符串长度)
/// </summary>
/// <param name="type">数据类型</param>
/// <param name="length">长度(对字符串类型有效)</param>
/// <returns>字节大小</returns>
public static int GetSize(Type type, int length)
{
if (type == typeof(string))
{
// S7 String = 2字节头部 + 实际字符数据
return 2 + length;
}
// 对于非字符串类型,忽略length参数
return GetSize(type);
}
}