Compare commits

...
2 Commits
Author SHA1 Message Date
jimmy 26a2661625 A:开放PLC定时循环参数 2025-12-11 15:12:49 +08:00
jimmy 5b7f127bb5 A:调整数据监听方案 2025-12-11 14:44:58 +08:00
11 changed files with 447 additions and 1 deletions

No files matched your search

+29
View File
@@ -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
+222
View File
@@ -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.
+3
View File
@@ -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地址格式
+10
View File
@@ -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);
}
+1
View File
@@ -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>
+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. 标签订阅与监控
+15 -1
View File
@@ -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)
{