A:开放PLC定时循环参数
This commit is contained in:
1 parent
5b7f127bb5
commit
26a2661625
9 files changed
+196
-1
No files matched your search
@@ -103,6 +103,9 @@ namespace Client
|
||||
{
|
||||
options.IpAddress = "127.0.0.1";
|
||||
options.CpuType = S7.Net.CpuType.S71200;
|
||||
// 配置监控循环频率 (单位: 毫秒)
|
||||
// 默认 200ms。如果设为 100ms 则频率更高,但需注意网络负载。
|
||||
options.MonitorInterval = 200;
|
||||
});
|
||||
|
||||
// WPF app services
|
||||
|
||||
@@ -4,6 +4,7 @@ using Plc.Abstractions;
|
||||
using Plc.Services;
|
||||
using Plc.Siemens;
|
||||
using Plc.Siemens.Options;
|
||||
using Plc.Options;
|
||||
|
||||
namespace Plc.Siemens.DependencyInjection;
|
||||
|
||||
@@ -26,6 +27,36 @@ public static class ServiceCollectionExtensions
|
||||
// 注意:这是一个共享实例,从 IOptions 获取值
|
||||
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)
|
||||
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)]
|
||||
public int MaxGapBytes { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 监控循环刷新率 (毫秒)。
|
||||
/// 默认为 200ms。
|
||||
/// </summary>
|
||||
[Range(10, 60000)]
|
||||
public int MonitorInterval { get; set; } = 200;
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
// 验证IP地址格式
|
||||
|
||||
@@ -54,9 +54,12 @@ services.AddSiemensPlc(options =>
|
||||
options.Slot = 1;
|
||||
options.PduSize = 960; // S7-1200/1500: 960, S7-300: 240
|
||||
options.MaxGapBytes = 20; // 批量优化间隔阈值
|
||||
options.MonitorInterval = 200; // 监控循环刷新率 (毫秒)
|
||||
});
|
||||
```
|
||||
|
||||
> **注意**:`MonitorInterval` 参数会被自动映射到核心服务 `PlcMonitorOptions.RefreshRate`。这是通过内部的 `Configure<PlcMonitorOptions>` 和 `IPostConfigureOptions` 机制实现的,用户无需手动配置核心选项。
|
||||
|
||||
### 完整示例
|
||||
|
||||
```csharp
|
||||
@@ -250,6 +253,13 @@ public class SiemensPlcOptions
|
||||
/// 默认: 20
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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. 标签订阅与监控
|
||||
|
||||
@@ -2,8 +2,10 @@ using Common.Results;
|
||||
using Plc.Abstractions;
|
||||
using Plc.Models;
|
||||
using Plc.Utils;
|
||||
using Plc.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Plc.Services;
|
||||
|
||||
@@ -13,11 +15,12 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
private readonly IPlcOptimizer _optimizer;
|
||||
private readonly IDataConverter _converter;
|
||||
private readonly ILogger<PlcMonitorService>? _logger;
|
||||
private readonly PlcMonitorOptions _options;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PlcTag> _subscriptions = new();
|
||||
private Task? _monitorTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(200);
|
||||
private readonly TimeSpan _refreshRate;
|
||||
|
||||
// 重连策略
|
||||
private int _reconnectAttempts = 0;
|
||||
@@ -27,12 +30,15 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
IPlcDriver driver,
|
||||
IPlcOptimizer optimizer,
|
||||
IDataConverter converter,
|
||||
IOptions<PlcMonitorOptions> options,
|
||||
ILogger<PlcMonitorService>? logger = null)
|
||||
{
|
||||
_driver = driver;
|
||||
_optimizer = optimizer;
|
||||
_converter = converter;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
_refreshRate = _options.RefreshRate;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
@@ -228,6 +234,7 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
currentTags.Count, batches.Count);
|
||||
|
||||
// 执行读取
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
foreach (var batch in batches)
|
||||
{
|
||||
if (token.IsCancellationRequested) break;
|
||||
@@ -244,6 +251,13 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user