A:优化PLC数据管理

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

No files matched your search

+32 -2
View File
@@ -7,6 +7,7 @@ using Plc.Models;
using Plc.Siemens.Options; using Plc.Siemens.Options;
using S7.Net; using S7.Net;
using System.Windows; using System.Windows;
using Sharp.Wpf.Abstractions;
namespace Client.ViewModels; namespace Client.ViewModels;
@@ -14,6 +15,7 @@ public class PlcViewModel : ViewModelBase
{ {
private readonly IPlcService _plcService; private readonly IPlcService _plcService;
private readonly SiemensPlcOptions _siemensOptions; private readonly SiemensPlcOptions _siemensOptions;
private readonly IDispatcher _dispatcher;
// 连接配置 // 连接配置
private PlcBrand _selectedBrand = PlcBrand.Siemens; private PlcBrand _selectedBrand = PlcBrand.Siemens;
@@ -30,10 +32,14 @@ public class PlcViewModel : ViewModelBase
// 监控 // 监控
private string _monitorAddress = "DB1.DBW0"; private string _monitorAddress = "DB1.DBW0";
public PlcViewModel(IPlcService plcService, SiemensPlcOptions siemensOptions) public PlcViewModel(
IPlcService plcService,
SiemensPlcOptions siemensOptions,
IDispatcher dispatcher)
{ {
_plcService = plcService; _plcService = plcService;
_siemensOptions = siemensOptions; _siemensOptions = siemensOptions;
_dispatcher = dispatcher;
// 初始化界面值为当前配置值 // 初始化界面值为当前配置值
_ipAddress = _siemensOptions.IpAddress; _ipAddress = _siemensOptions.IpAddress;
@@ -44,6 +50,7 @@ public class PlcViewModel : ViewModelBase
ReadCommand = new AsyncCommand(ReadAsync); ReadCommand = new AsyncCommand(ReadAsync);
WriteCommand = new AsyncCommand(WriteAsync); WriteCommand = new AsyncCommand(WriteAsync);
AddMonitorCommand = new RelayCommand(AddMonitor); AddMonitorCommand = new RelayCommand(AddMonitor);
RemoveMonitorCommand = new RelayCommand<PlcTag>(RemoveMonitor, tag => tag != null);
} }
#region Properties #region Properties
@@ -124,6 +131,7 @@ public class PlcViewModel : ViewModelBase
public IAsyncCommand ReadCommand { get; } public IAsyncCommand ReadCommand { get; }
public IAsyncCommand WriteCommand { get; } public IAsyncCommand WriteCommand { get; }
public ICommand AddMonitorCommand { get; } public ICommand AddMonitorCommand { get; }
public ICommand RemoveMonitorCommand { get; }
#endregion #endregion
@@ -201,12 +209,34 @@ public class PlcViewModel : ViewModelBase
DataType = typeof(short), DataType = typeof(short),
OnChanged = (t, v) => OnChanged = (t, v) =>
{ {
// 注意:UI绑定更新需要在UI线程,这里简化处理,假设WPF做了同步或者是后台线程安全集合 // 使用Dispatcher在UI线程更新值,避免跨线程访问异常
_dispatcher.InvokeAsync(() =>
{
t.Value = v; t.Value = v;
});
} }
}; };
_plcService.Subscribe(tag); _plcService.Subscribe(tag);
// 在UI线程添加到集合
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Add(tag); 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.DependencyInjection;
using Microsoft.Extensions.Options;
using Plc.Abstractions; using Plc.Abstractions;
using Plc.Services; using Plc.Services;
using Plc.Siemens; using Plc.Siemens;
@@ -8,15 +9,42 @@ namespace Plc.Siemens.DependencyInjection;
public static class ServiceCollectionExtensions 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(); // 使用标准的 IOptions 模式
configureOptions(options); services.Configure(configureOptions);
services.AddSingleton(options); // 同时注册 SiemensPlcOptions 单例,供需要直接访问的地方使用(如 PlcViewModel)
services.AddSingleton<IPlcDriver, SiemensDriver>(); // 注意:这是一个共享实例,从 IOptions 获取值
services.AddSingleton<IPlcOptimizer, SiemensOptimizer>(); 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>(); services.AddSingleton<IDataConverter, BigEndianConverter>();
// 注册PLC服务
services.AddSingleton<IPlcService, PlcMonitorService>(); services.AddSingleton<IPlcService, PlcMonitorService>();
return services; return services;
@@ -1,3 +1,5 @@
using Plc.Abstractions;
namespace Plc.Siemens.Models; namespace Plc.Siemens.Models;
public enum SiemensMemoryArea public enum SiemensMemoryArea
@@ -10,7 +12,7 @@ public enum SiemensMemoryArea
Counter = 0x1C Counter = 0x1C
} }
public class SiemensAddress public class SiemensAddress : IPlcAddress
{ {
public SiemensMemoryArea Area { get; set; } public SiemensMemoryArea Area { get; set; }
public int DbNumber { get; set; } // Only for DataBlock public int DbNumber { get; set; } // Only for DataBlock
@@ -1,18 +1,28 @@
using S7.Net; using S7.Net;
using System.ComponentModel.DataAnnotations;
using System.Net;
namespace Plc.Siemens.Options; namespace Plc.Siemens.Options;
public class SiemensPlcOptions public class SiemensPlcOptions : IValidatableObject
{ {
[Required]
public string IpAddress { get; set; } = "127.0.0.1"; public string IpAddress { get; set; } = "127.0.0.1";
[Range(1, 65535)]
public int Port { get; set; } = 102; public int Port { get; set; } = 102;
[Range(0, 255)]
public short Rack { get; set; } = 0; public short Rack { get; set; } = 0;
[Range(0, 255)]
public short Slot { get; set; } = 1; public short Slot { get; set; } = 1;
/// <summary> /// <summary>
/// S7-1200/1500 通常支持 960,S7-300 通常支持 240。 /// S7-1200/1500 通常支持 960,S7-300 通常支持 240。
/// 默认保守值设为 240。 /// 默认保守值设为 240。
/// </summary> /// </summary>
[Range(128, 960)]
public int PduSize { get; set; } = 240; public int PduSize { get; set; } = 240;
/// <summary> /// <summary>
@@ -20,4 +30,39 @@ public class SiemensPlcOptions
/// 默认为 S71200。 /// 默认为 S71200。
/// </summary> /// </summary>
public CpuType CpuType { get; set; } = CpuType.S71200; 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> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" /> <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" /> <PackageReference Include="S7netplus" Version="0.20.0" />
</ItemGroup> </ItemGroup>
+27 -12
View File
@@ -11,11 +11,10 @@ public class SiemensDriver : IPlcDriver
{ {
private readonly SiemensPlcOptions _options; private readonly SiemensPlcOptions _options;
private S7.Net.Plc? _plc; private S7.Net.Plc? _plc;
private readonly object _lock = new();
public SiemensDriver(SiemensPlcOptions options) public SiemensDriver(SiemensPlcOptions options)
{ {
_options = options; _options = options ?? throw new ArgumentNullException(nameof(options));
} }
public bool IsConnected => _plc?.IsConnected ?? false; public bool IsConnected => _plc?.IsConnected ?? false;
@@ -34,7 +33,12 @@ public class SiemensDriver : IPlcDriver
} }
// Create new instance with latest options // 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); await _plc.OpenAsync(cancellationToken);
@@ -45,34 +49,39 @@ public class SiemensDriver : IPlcDriver
} }
catch (Exception ex) 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 try
{ {
_plc?.Close(); _plc?.Close();
return await Task.FromResult(Result.Success()); return Task.FromResult(Result.Success());
} }
catch (Exception ex) 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) public async Task<Result<byte[]>> ReadBytesAsync(string address, ushort length, CancellationToken cancellationToken = default)
{ {
if (!IsConnected || _plc == null) if (!IsConnected || _plc == null)
return Result.Failure<byte[]>("PLC 未连接。"); return Result.Failure<byte[]>("PLC is not connected");
try try
{ {
var siemensAddr = SiemensAddressParser.Parse(address); var siemensAddr = SiemensAddressParser.Parse(address);
var dataType = MapToS7DataType(siemensAddr.Area); 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); return Result.Success(bytes);
} }
@@ -85,14 +94,19 @@ public class SiemensDriver : IPlcDriver
public async Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default) public async Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default)
{ {
if (!IsConnected || _plc == null) if (!IsConnected || _plc == null)
return Result.Failure("PLC 未连接。"); return Result.Failure("PLC is not connected");
try try
{ {
var siemensAddr = SiemensAddressParser.Parse(address); var siemensAddr = SiemensAddressParser.Parse(address);
var dataType = MapToS7DataType(siemensAddr.Area); 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(); return Result.Success();
} }
@@ -112,12 +126,13 @@ public class SiemensDriver : IPlcDriver
SiemensMemoryArea.Outputs => DataType.Output, SiemensMemoryArea.Outputs => DataType.Output,
SiemensMemoryArea.Timer => DataType.Timer, SiemensMemoryArea.Timer => DataType.Timer,
SiemensMemoryArea.Counter => DataType.Counter, SiemensMemoryArea.Counter => DataType.Counter,
_ => throw new ArgumentOutOfRangeException(nameof(area), area, null) _ => throw new ArgumentOutOfRangeException(nameof(area), area, $"Unsupported memory area: {area}")
}; };
} }
public void Dispose() public void Dispose()
{ {
_plc?.Close(); _plc?.Close();
_plc = null;
} }
} }
+33 -5
View File
@@ -3,13 +3,23 @@ using Plc.Models;
using Plc.Siemens.Models; using Plc.Siemens.Models;
using Plc.Siemens.Utils; using Plc.Siemens.Utils;
using Plc.Utils; using Plc.Utils;
using Microsoft.Extensions.Logging;
namespace Plc.Siemens; namespace Plc.Siemens;
public class SiemensOptimizer : IPlcOptimizer 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) public List<PlcReadBatch> Optimize(IEnumerable<PlcTag> tags, int maxPduSize)
{ {
@@ -24,16 +34,21 @@ public class SiemensOptimizer : IPlcOptimizer
try try
{ {
tag.ParsedAddress = SiemensAddressParser.Parse(tag.Address); 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; continue;
} }
} }
validTags.Add(tag); validTags.Add(tag);
} }
_logger?.LogDebug("Optimizing {ValidCount} valid tags out of {TotalCount} tags",
validTags.Count, tags.Count());
// 2. 按区域和 DB 号分组(物理位置) // 2. 按区域和 DB 号分组(物理位置)
var groups = validTags.GroupBy(t => var groups = validTags.GroupBy(t =>
{ {
@@ -46,12 +61,17 @@ public class SiemensOptimizer : IPlcOptimizer
// 3. 按字节偏移量排序 // 3. 按字节偏移量排序
var sortedTags = group.OrderBy(t => ((SiemensAddress)t.ParsedAddress!).ByteOffset).ToList(); 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; PlcReadBatch? currentBatch = null;
foreach (var tag in sortedTags) foreach (var tag in sortedTags)
{ {
var addr = (SiemensAddress)tag.ParsedAddress!; 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; // 此标签的总字节数 int byteLength = typeSize * tag.Length; // 此标签的总字节数
// 如果是 bool,它可能在一个字节内,但我们在检查边界时假设它占用该字节 // 如果是 bool,它可能在一个字节内,但我们在检查边界时假设它占用该字节
@@ -60,6 +80,7 @@ public class SiemensOptimizer : IPlcOptimizer
{ {
currentBatch = CreateBatch(tag, addr, byteLength); currentBatch = CreateBatch(tag, addr, byteLength);
batches.Add(currentBatch); batches.Add(currentBatch);
_logger?.LogTrace("Created new batch starting at {Address}", tag.Address);
continue; continue;
} }
@@ -77,20 +98,27 @@ public class SiemensOptimizer : IPlcOptimizer
// 1. 标签在当前结束之后(或位置相同)开始(因为已排序,所以是肯定的) // 1. 标签在当前结束之后(或位置相同)开始(因为已排序,所以是肯定的)
// 2. 间隔足够小 // 2. 间隔足够小
// 3. 总大小适合 PDU // 3. 总大小适合 PDU
if (gap >= 0 && gap <= MaxGapBytes && newTotalLength <= maxPduSize) if (gap >= 0 && gap <= _maxGapBytes && newTotalLength <= maxPduSize)
{ {
currentBatch.Tags.Add(tag); currentBatch.Tags.Add(tag);
currentBatch.TotalLength = newTotalLength; currentBatch.TotalLength = newTotalLength;
_logger?.LogTrace("Merged tag {TagId} into batch (gap={Gap}, newLength={Length})",
tag.Id, gap, newTotalLength);
} }
else 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); currentBatch = CreateBatch(tag, addr, byteLength);
batches.Add(currentBatch); batches.Add(currentBatch);
} }
} }
} }
_logger?.LogDebug("Optimization complete: {TagCount} tags -> {BatchCount} batches",
validTags.Count, batches.Count);
return batches; return batches;
} }
+17 -4
View File
@@ -2,9 +2,22 @@ namespace Plc.Abstractions;
public interface IDataConverter public interface IDataConverter
{ {
// 将原始字节(从指定偏移量开始)转换为目标类型 /// <summary>
object? Parse(byte[] buffer, int offset, Type type, int length); /// 将原始字节(从指定偏移量开始)转换为目标类型
/// </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>
byte[] ToBytes(object value, Type type, int length); /// 将值转换为字节
/// </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();
}
+2 -1
View File
@@ -38,6 +38,7 @@ public class PlcReadBatch
/// <summary> /// <summary>
/// 驱动程序特定的起始地址上下文(避免重新解析)。 /// 驱动程序特定的起始地址上下文(避免重新解析)。
/// 使用 IPlcAddress 接口以支持类型安全访问。
/// </summary> /// </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; namespace Plc.Models;
/// <summary> /// <summary>
@@ -39,6 +41,7 @@ public class PlcTag
/// <summary> /// <summary>
/// 内部使用:由驱动程序/优化器用于存储解析后的地址对象。 /// 内部使用:由驱动程序/优化器用于存储解析后的地址对象。
/// 这避免了在每个周期重新解析地址字符串。 /// 这避免了在每个周期重新解析地址字符串。
/// 使用 IPlcAddress 接口以支持类型安全访问。
/// </summary> /// </summary>
public object? ParsedAddress { get; set; } public IPlcAddress? ParsedAddress { get; set; }
} }
+4
View File
@@ -10,4 +10,8 @@
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project> </Project>
+109 -13
View File
@@ -5,54 +5,129 @@ namespace Plc.Services;
public class BigEndianConverter : IDataConverter 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)) if (type == typeof(bool))
return buffer[offset] > 0; // 简单的检查,真实的位逻辑需要位索引 {
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;
}
// Byte类型
if (type == typeof(byte)) if (type == typeof(byte))
{
if (offset >= buffer.Length)
throw new ArgumentException("Buffer too small for byte");
return buffer[offset]; return buffer[offset];
}
// Short类型
if (type == typeof(short)) if (type == typeof(short))
{
if (offset + 2 > buffer.Length)
throw new ArgumentException("Buffer too small for short");
return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset)); return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset));
}
// UShort类型
if (type == typeof(ushort)) if (type == typeof(ushort))
{
if (offset + 2 > buffer.Length)
throw new ArgumentException("Buffer too small for ushort");
return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset)); return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset));
}
// Int类型
if (type == typeof(int)) if (type == typeof(int))
{
if (offset + 4 > buffer.Length)
throw new ArgumentException("Buffer too small for int");
return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)); return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset));
}
// UInt类型
if (type == typeof(uint)) if (type == typeof(uint))
{
if (offset + 4 > buffer.Length)
throw new ArgumentException("Buffer too small for uint");
return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset)); return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset));
}
// Float类型
if (type == typeof(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))); return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)));
}
// Double类型
if (type == typeof(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))); return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(offset)));
}
// 简单处理字符串 (假设 ASCII/S7 String) // String类型(S7 String格式)
if (type == typeof(string)) if (type == typeof(string))
{ {
// S7 String 前两个字节是 MaxLen 和 ActualLen,后面是字符 // S7 String结构:
// 这里假设是纯字符数组或者需要特定处理 // 字节0: MaxLength
// 简单起见,按 ASCII 读取指定长度 // 字节1: ActualLength
return System.Text.Encoding.ASCII.GetString(buffer, offset, length); // 字节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);
} }
return null; 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)) if (value == null)
return new[] { (bool)value ? (byte)1 : (byte)0 }; 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)) if (type == typeof(byte))
return new[] { (byte)value }; return new[] { (byte)value };
// Short类型
if (type == typeof(short)) if (type == typeof(short))
{ {
var bytes = new byte[2]; var bytes = new byte[2];
@@ -60,6 +135,7 @@ public class BigEndianConverter : IDataConverter
return bytes; return bytes;
} }
// UShort类型
if (type == typeof(ushort)) if (type == typeof(ushort))
{ {
var bytes = new byte[2]; var bytes = new byte[2];
@@ -67,6 +143,7 @@ public class BigEndianConverter : IDataConverter
return bytes; return bytes;
} }
// Int类型
if (type == typeof(int)) if (type == typeof(int))
{ {
var bytes = new byte[4]; var bytes = new byte[4];
@@ -74,6 +151,7 @@ public class BigEndianConverter : IDataConverter
return bytes; return bytes;
} }
// UInt类型
if (type == typeof(uint)) if (type == typeof(uint))
{ {
var bytes = new byte[4]; var bytes = new byte[4];
@@ -81,6 +159,7 @@ public class BigEndianConverter : IDataConverter
return bytes; return bytes;
} }
// Float类型
if (type == typeof(float)) if (type == typeof(float))
{ {
var bytes = new byte[4]; var bytes = new byte[4];
@@ -89,6 +168,7 @@ public class BigEndianConverter : IDataConverter
return bytes; return bytes;
} }
// Double类型
if (type == typeof(double)) if (type == typeof(double))
{ {
var bytes = new byte[8]; var bytes = new byte[8];
@@ -97,6 +177,22 @@ public class BigEndianConverter : IDataConverter
return bytes; 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");
} }
} }
+163 -15
View File
@@ -3,6 +3,7 @@ using Plc.Abstractions;
using Plc.Models; using Plc.Models;
using Plc.Utils; using Plc.Utils;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
namespace Plc.Services; namespace Plc.Services;
@@ -11,91 +12,197 @@ public class PlcMonitorService : IPlcService, IDisposable
private readonly IPlcDriver _driver; private readonly IPlcDriver _driver;
private readonly IPlcOptimizer _optimizer; private readonly IPlcOptimizer _optimizer;
private readonly IDataConverter _converter; private readonly IDataConverter _converter;
private readonly ILogger<PlcMonitorService>? _logger;
private readonly ConcurrentDictionary<string, PlcTag> _subscriptions = new(); private readonly ConcurrentDictionary<string, PlcTag> _subscriptions = new();
private Task? _monitorTask; private Task? _monitorTask;
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(200); 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; _driver = driver;
_optimizer = optimizer; _optimizer = optimizer;
_converter = converter; _converter = converter;
_logger = logger;
} }
public async Task StartAsync(CancellationToken cancellationToken = default) public async Task StartAsync(CancellationToken cancellationToken = default)
{ {
if (_monitorTask != null) return; if (_monitorTask != null)
{
_logger?.LogWarning("Monitor service is already running");
return;
}
await _driver.ConnectAsync(cancellationToken); _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}");
}
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_monitorTask = Task.Run(() => MonitorLoop(_cts.Token), _cts.Token); _monitorTask = Task.Run(() => MonitorLoop(_cts.Token), _cts.Token);
_logger?.LogInformation("PLC monitor service started successfully");
} }
public async Task StopAsync() public async Task StopAsync()
{ {
_logger?.LogInformation("Stopping PLC monitor service");
_cts?.Cancel(); _cts?.Cancel();
if (_monitorTask != null) if (_monitorTask != null)
{ {
try { await _monitorTask; } catch (OperationCanceledException) { } try
{
await _monitorTask;
} }
catch (OperationCanceledException)
{
_logger?.LogDebug("Monitor task cancelled successfully");
}
}
await _driver.DisconnectAsync(); await _driver.DisconnectAsync();
_logger?.LogInformation("PLC monitor service stopped");
} }
public void Subscribe(PlcTag tag) 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) 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) public async Task<Result<T>> ReadAsync<T>(string address)
{ {
_logger?.LogDebug("Reading from address {Address}", address);
// 临时读取(未优化) // 临时读取(未优化)
int size = TypeHelper.GetSize(typeof(T)); int size = TypeHelper.GetSize(typeof(T));
var result = await _driver.ReadBytesAsync(address, (ushort)size); 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"); return Result.Failure<T>(result.ErrorMessage ?? result.Exception?.Message ?? "Read failed");
}
if (result.Value is null) if (result.Value is null)
{
_logger?.LogError("Read from {Address} returned null data", address);
return Result.Failure<T>("Read returned null data"); return Result.Failure<T>("Read returned null data");
}
var val = _converter.Parse(result.Value, 0, typeof(T), 1); var val = _converter.Parse(result.Value, 0, typeof(T), 1);
if (val is null) if (val is null)
{
_logger?.LogError("Failed to convert data from {Address}", address);
return Result.Failure<T>("Conversion failed"); return Result.Failure<T>("Conversion failed");
}
_logger?.LogDebug("Successfully read value from {Address}", address);
return Result.Success((T)val); return Result.Success((T)val);
} }
public async Task<Result> WriteAsync<T>(string address, T value) public async Task<Result> WriteAsync<T>(string address, T value)
{ {
if (value == null) if (value == null)
{
_logger?.LogWarning("Attempted to write null value to {Address}", address);
return Result.Failure("Cannot write null value"); return Result.Failure("Cannot write null value");
}
_logger?.LogDebug("Writing to address {Address}", address);
int size = TypeHelper.GetSize(typeof(T)); int size = TypeHelper.GetSize(typeof(T));
var bytes = _converter.ToBytes(value, typeof(T), size); var bytes = _converter.ToBytes(value, typeof(T), size);
if (bytes.Length == 0) 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."); return Result.Failure("Conversion to bytes returned empty array. Type might not be supported.");
}
return await _driver.WriteBytesAsync(address, bytes); 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) private async Task MonitorLoop(CancellationToken token)
{ {
_logger?.LogInformation("Monitor loop started");
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
try try
{ {
// 连接检查和重试逻辑
if (!_driver.IsConnected) 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(); var currentTags = _subscriptions.Values.ToList();
@@ -107,6 +214,8 @@ public class PlcMonitorService : IPlcService, IDisposable
// 优化:计算分桶 // 优化:计算分桶
var batches = _optimizer.Optimize(currentTags, _driver.MaxPduSize); var batches = _optimizer.Optimize(currentTags, _driver.MaxPduSize);
_logger?.LogTrace("Optimized {TagCount} tags into {BatchCount} batches",
currentTags.Count, batches.Count);
// 执行读取 // 执行读取
foreach (var batch in batches) foreach (var batch in batches)
@@ -121,48 +230,87 @@ public class PlcMonitorService : IPlcService, IDisposable
} }
else 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); await Task.Delay(_refreshRate, token);
} }
_logger?.LogInformation("Monitor loop stopped");
} }
private void DistributeValues(PlcReadBatch batch, byte[] data) 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; int batchStartByte = batchStart.ByteOffset;
foreach (var tag in batch.Tags) 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 tagStartByte = tagAddr.ByteOffset;
int relativeOffset = tagStartByte - batchStartByte; 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) if (relativeOffset + typeSize <= data.Length)
{ {
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length); try
{
// 传递位偏移量以支持bool类型
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length, tagAddr.BitOffset);
// 变更检测 // 变更检测
if (!Equals(tag.Value, newVal)) if (!Equals(tag.Value, newVal))
{ {
tag.Value = newVal; tag.Value = newVal;
tag.OnChanged?.Invoke(tag, 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() public void Dispose()
{ {
_logger?.LogDebug("Disposing PLC monitor service");
_cts?.Cancel(); _cts?.Cancel();
_driver.Dispose(); _driver.Dispose();
} }
+34 -2
View File
@@ -2,14 +2,46 @@ namespace Plc.Utils;
public static class TypeHelper public static class TypeHelper
{ {
/// <summary>
/// 获取PLC数据类型的字节大小
/// </summary>
/// <param name="type">数据类型</param>
/// <returns>字节大小</returns>
/// <exception cref="ArgumentException">当类型不支持或需要明确长度时抛出</exception>
public static int GetSize(Type type) 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(byte)) return 1;
if (type == typeof(short) || type == typeof(ushort)) return 2; if (type == typeof(short) || type == typeof(ushort)) return 2;
if (type == typeof(int) || type == typeof(uint) || type == typeof(float)) return 4; if (type == typeof(int) || type == typeof(uint) || type == typeof(float)) return 4;
if (type == typeof(long) || type == typeof(ulong) || type == typeof(double)) return 8; 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);
} }
} }