A:开放PLC定时循环参数

This commit is contained in:
jimmy committed 2025-12-11 15:12:49 +08:00
1 parent 5b7f127bb5
commit 26a2661625
9 files changed
+196 -1

No files matched your search

+3
View File
@@ -103,6 +103,9 @@ namespace Client
{ {
options.IpAddress = "127.0.0.1"; options.IpAddress = "127.0.0.1";
options.CpuType = S7.Net.CpuType.S71200; options.CpuType = S7.Net.CpuType.S71200;
// 配置监控循环频率 (单位: 毫秒)
// 默认 200ms。如果设为 100ms 则频率更高,但需注意网络负载。
options.MonitorInterval = 200;
}); });
// WPF app services // WPF app services
@@ -4,6 +4,7 @@ using Plc.Abstractions;
using Plc.Services; using Plc.Services;
using Plc.Siemens; using Plc.Siemens;
using Plc.Siemens.Options; using Plc.Siemens.Options;
using Plc.Options;
namespace Plc.Siemens.DependencyInjection; namespace Plc.Siemens.DependencyInjection;
@@ -26,6 +27,36 @@ public static class ServiceCollectionExtensions
// 注意:这是一个共享实例,从 IOptions 获取值 // 注意:这是一个共享实例,从 IOptions 获取值
services.AddSingleton(sp => sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value); services.AddSingleton(sp => sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value);
// 将 SiemensPlcOptions.MonitorInterval 映射到 PlcMonitorOptions
services.Configure<PlcMonitorOptions>(options =>
{
// 这里我们无法直接获取 IOptions<SiemensPlcOptions>,因为 Configure 是在构建容器前执行的
// 所以我们创建一个临时的 SiemensPlcOptions 来获取配置
// 但更好的方式是让 PlcMonitorOptions 的配置源自 SiemensPlcOptions 的配置源
// 简单起见,我们在这里通过回调再次执行 configureOptions
var tempOptions = new SiemensPlcOptions();
configureOptions(tempOptions);
options.RefreshRate = TimeSpan.FromMilliseconds(tempOptions.MonitorInterval);
});
// 注意:上面的映射方式有一个小缺陷,如果 SiemensPlcOptions 是通过 appsettings.json 绑定的,
// 那么这里的 configureOptions 可能只是部分配置或为空。
// 更稳健的方式是使用 PostConfigure
services.PostConfigure<PlcMonitorOptions>(options =>
{
// 这种方式需要通过 ServiceProvider 获取 SiemensPlcOptions,但在 PostConfigure 中拿不到 ServiceProvider
// 所以通常我们在注册服务工厂时手动转换,或者使用 OptionsMonitor
});
// 修正:在注册 IPlcService 时,其实不需要显式配置 PlcMonitorOptions,
// 只要我们确保 PlcMonitorOptions 被正确配置即可。
// 由于 configureOptions 委托是用户传进来的,我们再次利用它配置 PlcMonitorOptions 是最直接的。
// 但如果用户用了 Configuration Bind,那么 configureOptions 可能是空的。
// 更好的方案:使用 IPostConfigureOptions<PlcMonitorOptions>
services.AddSingleton<IPostConfigureOptions<PlcMonitorOptions>, SiemensPlcMonitorOptionsConfigurator>();
// 注册驱动程序(使用 IOptions) // 注册驱动程序(使用 IOptions)
services.AddSingleton<IPlcDriver>(sp => services.AddSingleton<IPlcDriver>(sp =>
{ {
@@ -0,0 +1,20 @@
using Microsoft.Extensions.Options;
using Plc.Options;
namespace Plc.Siemens.Options;
public class SiemensPlcMonitorOptionsConfigurator : IPostConfigureOptions<PlcMonitorOptions>
{
private readonly IOptionsMonitor<SiemensPlcOptions> _siemensOptions;
public SiemensPlcMonitorOptionsConfigurator(IOptionsMonitor<SiemensPlcOptions> siemensOptions)
{
_siemensOptions = siemensOptions;
}
public void PostConfigure(string? name, PlcMonitorOptions options)
{
// 从 SiemensPlcOptions 同步 MonitorInterval 到 PlcMonitorOptions
options.RefreshRate = TimeSpan.FromMilliseconds(_siemensOptions.CurrentValue.MonitorInterval);
}
}
@@ -39,6 +39,13 @@ public class SiemensPlcOptions : IValidatableObject
[Range(0, 1000)] [Range(0, 1000)]
public int MaxGapBytes { get; set; } = 20; public int MaxGapBytes { get; set; } = 20;
/// <summary>
/// 监控循环刷新率 (毫秒)。
/// 默认为 200ms。
/// </summary>
[Range(10, 60000)]
public int MonitorInterval { get; set; } = 200;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
// 验证IP地址格式 // 验证IP地址格式
+10
View File
@@ -54,9 +54,12 @@ services.AddSiemensPlc(options =>
options.Slot = 1; options.Slot = 1;
options.PduSize = 960; // S7-1200/1500: 960, S7-300: 240 options.PduSize = 960; // S7-1200/1500: 960, S7-300: 240
options.MaxGapBytes = 20; // 批量优化间隔阈值 options.MaxGapBytes = 20; // 批量优化间隔阈值
options.MonitorInterval = 200; // 监控循环刷新率 (毫秒)
}); });
``` ```
> **注意**:`MonitorInterval` 参数会被自动映射到核心服务 `PlcMonitorOptions.RefreshRate`。这是通过内部的 `Configure<PlcMonitorOptions>` 和 `IPostConfigureOptions` 机制实现的,用户无需手动配置核心选项。
### 完整示例 ### 完整示例
```csharp ```csharp
@@ -250,6 +253,13 @@ public class SiemensPlcOptions
/// 默认: 20 /// 默认: 20
/// </summary> /// </summary>
public int MaxGapBytes { get; set; } public int MaxGapBytes { get; set; }
/// <summary>
/// 监控循环刷新率 (毫秒)
/// 验证: 10-60000
/// 默认: 200
/// </summary>
public int MonitorInterval { get; set; }
} }
``` ```
@@ -0,0 +1,9 @@
namespace Plc.Options;
public class PlcMonitorOptions
{
/// <summary>
/// 监控循环刷新率 (毫秒)
/// </summary>
public TimeSpan RefreshRate { get; set; } = TimeSpan.FromMilliseconds(200);
}
+1
View File
@@ -12,6 +12,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+100
View File
@@ -279,6 +279,106 @@ public class OmronOptimizer : IPlcOptimizer
--- ---
## 📅 分级订阅方案 (Proposed Tiered Subscription)
> 本节描述了未来支持不同频率订阅(快/慢循环)的实现思路,目前尚未实装。
### 1. 需求背景
在复杂的工业场景中,不同数据的实时性要求不同:
- **关键控制信号**(如 启动/停止/报警):需要高频刷新(100ms)。
- **普通模拟量**(如 温度/液位):变化较慢,只需低频刷新(1000ms)。
- **配方/参数**(如 设定值):只需在变动时读取,或极低频刷新(5000ms)。
当前单一循环机制(统一刷新率)会导致资源浪费或关键数据延迟。
### 2. 接口设计变更
在订阅时增加 `Priority` 或 `Interval` 参数:
```csharp
public enum MonitorPriority
{
High, // 100ms
Normal, // 200ms (默认)
Low // 1000ms
}
public class PlcTag
{
// ... 原有属性
public MonitorPriority Priority { get; set; } = MonitorPriority.Normal;
}
// 或者在 Subscribe 时指定
_plcService.Subscribe(tag, MonitorPriority.High);
```
### 3. 实现策略
#### 方案 A:多循环线程 (Multi-Loop)
`PlcMonitorService` 内部维护多个 `MonitorLoop` 任务,每个任务对应一个优先级队列。
* **HighPriorityTask**: `While(true) { Read(HighTags); Delay(100ms); }`
* **LowPriorityTask**: `While(true) { Read(LowTags); Delay(1000ms); }`
**优点**:逻辑清晰,互不干扰。
**缺点**:如果底层驱动不支持并发(非线程安全),需要加锁,导致高优任务被低优任务阻塞。S7.Net 库通常不支持并发读写,需要加全局锁。
#### 方案 B:加权轮询 (Weighted Round-Robin)
在单个 `MonitorLoop` 中通过计数器控制读取频率。假设基准频率为 100ms。
* **Tick 1**: 读取 HighTags
* **Tick 2**: 读取 HighTags
* **Tick 3**: 读取 HighTags
* **...**
* **Tick 10**: 读取 HighTags + LowTags (每10次读取一次低优)
**伪代码:**
```csharp
long tick = 0;
while (!token.IsCancellationRequested)
{
var highTags = _subscriptions.Where(t => t.Priority == High);
var lowTags = _subscriptions.Where(t => t.Priority == Low);
var tagsToRead = new List<PlcTag>(highTags);
// 每 10 个周期 (10 * 100ms = 1s) 读取一次低频数据
if (tick % 10 == 0)
{
tagsToRead.AddRange(lowTags);
}
// 统一优化和读取
var batches = _optimizer.Optimize(tagsToRead, ...);
await ExecuteBatches(batches);
tick++;
await Task.Delay(100);
}
```
**优点**:单线程模型,无并发冲突,驱动兼容性好。
**缺点**:如果低频数据量极大,在 Tick 10 这一轮会耗时较长,可能导致该轮的高频数据延迟。可以通过将低频数据分片(Time-Slicing)分散到不同 Tick 来解决。
### 4. 配置扩展
在 `SiemensPlcOptions` 中增加各级频率配置:
```csharp
public class SiemensPlcOptions
{
public int HighInterval { get; set; } = 100;
public int NormalInterval { get; set; } = 200;
public int LowInterval { get; set; } = 1000;
}
```
---
## 🎯 高级功能 ## 🎯 高级功能
### 1. 标签订阅与监控 ### 1. 标签订阅与监控
+15 -1
View File
@@ -2,8 +2,10 @@ using Common.Results;
using Plc.Abstractions; using Plc.Abstractions;
using Plc.Models; using Plc.Models;
using Plc.Utils; using Plc.Utils;
using Plc.Options;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Plc.Services; namespace Plc.Services;
@@ -13,11 +15,12 @@ public class PlcMonitorService : IPlcService, IDisposable
private readonly IPlcOptimizer _optimizer; private readonly IPlcOptimizer _optimizer;
private readonly IDataConverter _converter; private readonly IDataConverter _converter;
private readonly ILogger<PlcMonitorService>? _logger; private readonly ILogger<PlcMonitorService>? _logger;
private readonly PlcMonitorOptions _options;
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;
// 重连策略 // 重连策略
private int _reconnectAttempts = 0; private int _reconnectAttempts = 0;
@@ -27,12 +30,15 @@ public class PlcMonitorService : IPlcService, IDisposable
IPlcDriver driver, IPlcDriver driver,
IPlcOptimizer optimizer, IPlcOptimizer optimizer,
IDataConverter converter, IDataConverter converter,
IOptions<PlcMonitorOptions> options,
ILogger<PlcMonitorService>? logger = null) ILogger<PlcMonitorService>? logger = null)
{ {
_driver = driver; _driver = driver;
_optimizer = optimizer; _optimizer = optimizer;
_converter = converter; _converter = converter;
_logger = logger; _logger = logger;
_options = options.Value;
_refreshRate = _options.RefreshRate;
} }
public async Task StartAsync(CancellationToken cancellationToken = default) public async Task StartAsync(CancellationToken cancellationToken = default)
@@ -228,6 +234,7 @@ public class PlcMonitorService : IPlcService, IDisposable
currentTags.Count, batches.Count); currentTags.Count, batches.Count);
// 执行读取 // 执行读取
var sw = System.Diagnostics.Stopwatch.StartNew();
foreach (var batch in batches) foreach (var batch in batches)
{ {
if (token.IsCancellationRequested) break; if (token.IsCancellationRequested) break;
@@ -244,6 +251,13 @@ public class PlcMonitorService : IPlcService, IDisposable
batch.StartAddress, readResult.ErrorMessage); batch.StartAddress, readResult.ErrorMessage);
} }
} }
sw.Stop();
if (sw.Elapsed > _refreshRate)
{
_logger?.LogWarning("Monitor loop took {Elapsed} ms, which is longer than refresh rate {Rate} ms",
sw.Elapsed.TotalMilliseconds, _refreshRate.TotalMilliseconds);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {