Compare commits
2
Commits
ea6103a596
...
26a2661625
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a2661625 | ||
|
|
5b7f127bb5 |
No files matched your search
+29
@@ -0,0 +1,29 @@
|
||||
# Visual Studio
|
||||
.vs/
|
||||
bin/
|
||||
obj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
*.user
|
||||
*.suo
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
*.sln.docstates
|
||||
*.dll
|
||||
*.pdb
|
||||
*.exe
|
||||
!src/**/bin/**/*.dll
|
||||
!src/**/bin/**/*.pdb
|
||||
!src/**/bin/**/*.exe
|
||||
|
||||
# Rider
|
||||
.idea/
|
||||
|
||||
# Dotnet
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Other
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
@@ -0,0 +1,222 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Sharp-Basic is a modular C# .NET 8 framework for building enterprise applications with reusable components. It provides a comprehensive toolkit with emphasis on:
|
||||
- **Modularity**: Independent, reusable libraries that can be used selectively
|
||||
- **Desktop Applications**: WPF-based UI toolkit with MVVM infrastructure
|
||||
- **Industrial IoT**: PLC integration (Siemens) and serial port communication
|
||||
- **Configuration as Code**: Multi-format (JSON/XML/INI/TOML) with hot-reload
|
||||
- **Functional Programming**: Result<T> pattern for railway-oriented error handling
|
||||
|
||||
**Language**: All comments and documentation are in Chinese (中文).
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Build the entire solution
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet build Sharp.sln
|
||||
```
|
||||
|
||||
### Build specific project
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet build Client/Client.csproj # WPF demo application
|
||||
dotnet build Common/Common.csproj # Core utilities
|
||||
dotnet build Config/Config.csproj # Configuration management
|
||||
```
|
||||
|
||||
### Run tests
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet test Sharp.sln # All tests
|
||||
dotnet test ../../test/Common.Test # Specific test project
|
||||
```
|
||||
|
||||
### Run the WPF Client application
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet run --project Client/Client.csproj
|
||||
```
|
||||
|
||||
### Clean build artifacts
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet clean Sharp.sln
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/Sharp/ # All source projects
|
||||
├── Common/ # Core foundation (Result<T>, Global container, utilities)
|
||||
├── Config/ # Multi-format configuration with hot-reload
|
||||
├── Sharp.Infrastructure/ # Infrastructure abstractions
|
||||
├── Wpf/ # WPF UI toolkit (MVVM, dialogs, converters)
|
||||
├── Data/ # EF Core wrapper with repository pattern
|
||||
├── Repository/ # Repository implementations
|
||||
├── Log/ # Serilog-based structured logging
|
||||
├── NetClient/ # HTTP client with Polly resilience
|
||||
├── NetServer/ # Lightweight HTTP server with middleware
|
||||
├── Ports/ # Serial port abstraction (RS232/RS485)
|
||||
├── Plc/ # PLC abstraction interface
|
||||
├── Plc.Siemens/ # Siemens S7 PLC implementation (S7.NET+)
|
||||
├── EventBus/ # Event pub/sub (planned)
|
||||
├── Client/ # WPF demo application
|
||||
└── Server/ # WPF server example
|
||||
|
||||
test/ # MSTest test projects
|
||||
├── Common.Test/
|
||||
├── Config.Test/
|
||||
├── Data.Test/
|
||||
├── Log.Test/
|
||||
├── NetClient.Test/
|
||||
└── NetServer.Test/
|
||||
```
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
#### 1. Result<T> Pattern (Railway-Oriented Programming)
|
||||
- Eliminates exceptions as control flow
|
||||
- Provides chainable operations: `Map`, `Bind`, `Match`, `Where`, `OnSuccess`, `OnFailure`
|
||||
- Synchronous and async wrappers: `Result.Try()`, `Result.TryAsync()`
|
||||
- Example usage:
|
||||
```csharp
|
||||
Result.Try(() => int.Parse(input))
|
||||
.Map(x => x * 2)
|
||||
.Where(v => v > 100, "Value too small")
|
||||
.OnSuccess(v => Console.WriteLine(v))
|
||||
.OnFailure((ex, msg) => logger.LogError(msg));
|
||||
```
|
||||
|
||||
#### 2. Dependency Injection (Hybrid Approach)
|
||||
- **Primary**: Microsoft.Extensions.DependencyInjection for standard DI
|
||||
- **Fallback**: Global service locator (Common.Global) for legacy/convenience
|
||||
- Services registered in `DependencyInjection` folders within each module
|
||||
- Client app startup (App.xaml.cs) orchestrates all service registration
|
||||
|
||||
#### 3. Configuration Management
|
||||
- Multi-source configuration builder supporting JSON/XML/INI/TOML
|
||||
- Hot-reload with change notifications
|
||||
- Strong typing with DataAnnotations validation
|
||||
- IOptions<T> pattern integration
|
||||
- Environment variable overrides with optional prefix
|
||||
|
||||
#### 4. Repository Pattern with EF Core
|
||||
- Generic `IRepository<TEntity>` abstraction
|
||||
- Built-in entity types: `Entity`, `AuditableEntity`, `ISoftDeletable`
|
||||
- Auto-audit tracking (CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
|
||||
- Multi-database support: SQL Server, SQLite, PostgreSQL, MySQL, In-Memory
|
||||
|
||||
#### 5. MVVM for WPF
|
||||
- `ViewModelBase` with INotifyPropertyChanged and IsBusy tracking
|
||||
- `RelayCommand` and `AsyncCommand` implementations
|
||||
- Services: `IDialogService`, `IFileDialogService`, `IDispatcherService`
|
||||
- Converters: BoolToVisibility, InverseBooleanConverter, EnumToDescriptionConverter
|
||||
- Global exception handling for UI/Task/AppDomain exceptions
|
||||
|
||||
#### 6. Middleware Pattern
|
||||
- Custom HTTP server (NetServer) with ASP.NET Core-style pipeline
|
||||
- Middleware: Authentication, CORS, Rate Limiting, Caching, Logging, Exception Handling
|
||||
- Attribute-based routing with `RouteAttribute`
|
||||
- Custom `IMiddleware` interface for extensibility
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
**Core Layer** (no external dependencies):
|
||||
- Common → (no dependencies)
|
||||
|
||||
**Configuration Layer**:
|
||||
- Config → Microsoft.Extensions.Configuration.*, Tomlyn (TOML support)
|
||||
|
||||
**Data Layer**:
|
||||
- Data → EF Core, Common
|
||||
- Repository → Data
|
||||
|
||||
**Network Layer**:
|
||||
- NetClient → Polly, Microsoft.Extensions.Http, Common
|
||||
- NetServer → Common, Log
|
||||
|
||||
**WPF Layer**:
|
||||
- Wpf → HandyControl (optional UI controls)
|
||||
- Client → Wpf, Config, NetClient, NetServer, Log, Plc.Siemens, Ports
|
||||
|
||||
**Industrial IoT**:
|
||||
- Ports → (serial port communication)
|
||||
- Plc → (abstraction only)
|
||||
- Plc.Siemens → Plc, S7.NET+ library
|
||||
|
||||
**Logging**:
|
||||
- Log → Serilog, Microsoft.Extensions.Logging
|
||||
|
||||
## Common Development Workflows
|
||||
|
||||
### Adding a new module/library
|
||||
1. Create project under `src/Sharp/`
|
||||
2. Target `net8.0` (or `net8.0-windows` for WPF)
|
||||
3. Enable nullable reference types: `<Nullable>enable</Nullable>`
|
||||
4. Create `DependencyInjection` folder for service registration extensions
|
||||
5. Add test project under `test/` with MSTest
|
||||
6. Add to Sharp.sln: `dotnet sln add <ProjectPath>`
|
||||
|
||||
### Working with Result<T>
|
||||
- Use `Result.Try()` to wrap exception-prone code
|
||||
- Chain operations with `Map()` for transformations
|
||||
- Use `Bind()` for operations that return Result<T>
|
||||
- Extract values with `GetOrDefault()`, `GetOrThrow()`, or `Match()`
|
||||
- Always handle both success and failure cases
|
||||
|
||||
### Using the Global container
|
||||
- Register singletons: `Global.RegisterSingleton<T>(instance)` or `Global.RegisterSingleton<T>(() => new T())`
|
||||
- Register transients: `Global.RegisterTransient<T>(() => new T())`
|
||||
- Subscribe to changes: `Global.Subscribe<T>(e => { /* handle change */ })`
|
||||
- Retrieve services: `Global.Get<T>()`
|
||||
- Prefer standard DI (IServiceCollection) for new code
|
||||
|
||||
### Configuration workflow
|
||||
1. Add config files to project root (appsettings.json, etc.)
|
||||
2. Create strongly-typed options class with DataAnnotations
|
||||
3. Build ConfigManager in App.xaml.cs OnStartup
|
||||
4. Register with DI: `services.AddSingleton(cfg)` and `services.AddSingleton(cfg.Configuration)`
|
||||
5. Use hot-reload callbacks: `cfg.OnReload(() => { /* refresh */ })`
|
||||
|
||||
### WPF Client startup flow
|
||||
The Client application (App.xaml.cs) orchestrates initialization:
|
||||
1. Register global exception handlers (UI, Task, AppDomain)
|
||||
2. Build ConfigManager from appsettings.json
|
||||
3. Setup ServiceCollection and register all services
|
||||
4. Create ServiceProvider
|
||||
5. Show LoginWindow (modal)
|
||||
6. On successful login, show MainWindow
|
||||
|
||||
### Working with PLC (Siemens)
|
||||
- Interface: `IPLC` in Plc project
|
||||
- Implementation: Plc.Siemens using S7.NET+ library
|
||||
- Supports: S7-1200, S7-1500, S7-300, S7-400
|
||||
- Register with DI: Add Siemens PLC service with connection options
|
||||
- Methods: Read/Write memory areas, data blocks, etc.
|
||||
|
||||
### Serial Port Communication
|
||||
- Service: `ISerialPortService` in Ports module
|
||||
- Discovery: `SerialPortDiscovery` for auto-detection
|
||||
- Event-based data reception: `DataReceivedEventArgs`
|
||||
- Configured with: baud rate, parity, stop bits, data bits
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Target Framework**: .NET 8 (net8.0 / net8.0-windows)
|
||||
- **Nullable**: Enabled (`#nullable enable`)
|
||||
- **Implicit Usings**: Enabled
|
||||
- **Namespace Convention**: `Sharp.*` (Common will migrate to `Sharp.Common`)
|
||||
- **Language**: All comments, docs, and strings in Chinese
|
||||
- **Backwards Compatibility**: Maintain for minor versions; breaking changes only in major versions
|
||||
- **Testing**: MSTest with parallel execution enabled
|
||||
|
||||
## License
|
||||
|
||||
GNU AGPLv3 - Network services that use this framework must open source modifications. Contact author for alternative commercial licensing.
|
||||
@@ -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