半导体 / OLED 检测量测设备的三层级配置管理框架,提供系统级 → 设备级 → 配方级完整的参数管理能力,支持版本迁移、快照差异、AES 加密、SECS/GEM 参数映射及 Avalonia UI 集成。
┌─────────────────────────────────────────────────┐
│ 应用层 (Demo / 设备应用) │
├─────────────────────────────────────────────────┤
│ CNS.Core.Configuration.Autofac │ ← DI 集成 / 文件注入
│ CNS.Core.Configuration.Mvvm │ ← MVVM 绑定基类
├─────────────────────────────────────────────────┤
│ CNS.Core.Configuration │ ← 核心库
│ ┌──────────┬──────────┬────────────────────┐ │
│ │ System │ Base │ Recipe │ │
│ │ 系统配置 │ 设备配置 │ 配方配置(可切换) │ │
│ └──────────┴──────────┴────────────────────┘ │
│ 持久化 / 验证 / 快照 / 审计 / 加密 / 版本迁移 │
└─────────────────────────────────────────────────┘
| 层级 | 对象类型 | 存储路径 | 可切换 | 典型用途 |
|---|---|---|---|---|
| System | ISettingsFileOperation |
单一 JSON 文件 | ✗ | 软件平台参数、端口、路径 |
| Base | IBaseConfigure 集合 |
base.json |
✗ | 相机标定、运动轴参数、光源参数 |
| Recipe | IRecipeConfigure 集合 |
recipes/<id>.json |
✓ | 视觉算法、检测阈值、工艺参数 |
指针稳定性:切换配方时通过 IObjectMapper 原地属性赋值,DI 容器中的对象引用始终不变,UI 绑定无需重建。
CNS.Core.Configuration/
├── src/
│ ├── CNS.Core.Configuration/ 核心库(net462;net8.0)
│ │ ├── Abstractions/ 23 个核心接口
│ │ ├── Base/ ConfigureBase、Composite/List 变体
│ │ ├── Store/ ConfigurationStoreBase、InMemoryContainer
│ │ ├── Recipe/ RecipeConfiguration、RecipeMetadata
│ │ ├── Serialization/ ConfigureJsonConverter、报告
│ │ ├── Validation/ ValidationPipeline、跨节点验证
│ │ ├── ChangeDetection/ 快照、Undo/Redo、变更检测
│ │ ├── Events/ ConfigEventBus、ConfigChangedEvent
│ │ ├── Audit/ FileAuditTrailService
│ │ ├── Security/ 访问控制、AES 加密、锁定
│ │ ├── Backup/ 双路径备份服务
│ │ ├── Migration/ 链式版本迁移
│ │ ├── Mapping/ 集合映射策略
│ │ ├── Secs/ SECS/GEM 参数注册
│ │ ├── Format/ XML/CSV/SECS 格式转换
│ │ ├── Diagnostics/ 健康报告、性能指标
│ │ ├── SystemConfig/ JsonFileOperation
│ │ └── Bootstrapper/ 三层级初始化编排
│ │
│ ├── CNS.Core.Configuration.Mvvm/ MVVM 绑定层(net462;net8.0)
│ │ └── BindableConfigureBase SetProperty 自动标记 IsDirty
│ │
│ └── CNS.Core.Configuration.Autofac/ DI 集成(net462;net8.0)
│ ├── ConfigurationServiceModule 一次性服务注册
│ ├── FileInjectionModule JSON 驱动的依赖注入
│ └── ConfigureContainerPopulator IStartable 自动填充
│
├── tests/
│ ├── CNS.Core.Configuration.Tests/ 单元测试(net8.0)
│ └── CNS.Core.Configuration.Autofac.Tests/ Autofac 集成测试(net8.0)
│
├── demo/
│ └── CNS.Core.Configuration.Demo/ Avalonia 演示应用(net8.0)
│
└── docs/ 设计文档
| # | 特性 | 说明 |
|---|---|---|
| 1 | 配方切换 | 按产品/批次切换,指针不变,UI 绑定不刷新 |
| 2 | 配方库 CRUD | 创建、复制、删除、导入、导出、历史记录 |
| 3 | 快照与差异 | 任意时刻截取配置状态,对比两个快照的属性变化 |
| 4 | 属性验证 | Critical / Error / Warning 三级,支持 [ParameterRange] 标注 |
| 5 | Undo / Redo | 会话级命令模式,支持多步撤销/重做 |
| 6 | AES-256 加密 | [Sensitive] 标注属性自动加密存储($enc:AES256:base64) |
| 7 | 访问控制 | Operator / Engineer / Admin 三级角色,按层级授权 |
| 8 | 配置锁定 | 生产运行期间阻止任何修改 |
| 9 | 审计追踪 | 追加写入审计日志(谁、何时、改了什么、改成什么) |
| 10 | 双路径备份 | 本地路径 + AppData 同步备份,崩溃恢复 _working.json |
| 11 | 链式版本迁移 | 按版本号自动升级旧配置文件 |
| 12 | SECS/GEM 参数映射 | [SecsParameter] 属性标注,支持 S2F13 查询 / S2F15 设置 |
| 13 | 格式转换 | 一键导出 XML / CSV / SECS Binary |
| 14 | 文件注入 | JSON 驱动的 Autofac 构造函数/属性/集合注入 |
| 15 | 批量操作 | BeginBatch/EndBatch 合并事件,单次 UI 刷新 |
<!-- 核心库 -->
<PackageReference Include="CNS.Core.Configuration" Version="*" />
<!-- MVVM 支持(Avalonia / WPF) -->
<PackageReference Include="CNS.Core.Configuration.Mvvm" Version="*" />
<!-- Autofac DI 集成(可选) -->
<PackageReference Include="CNS.Core.Configuration.Autofac" Version="*" />// 设备级配置(Base 层)
public class CameraConfig : BaseConfigureBase
{
private double _exposureTime = 5000;
[ParameterRange(100, 100000)]
public double ExposureTime
{
get => _exposureTime;
set => SetConfigProperty(ref _exposureTime, value);
}
}
// 配方级配置(Recipe 层)
public class InspectionAlgorithmConfig : RecipeConfigureBase
{
private double _threshold = 0.5;
[ParameterRange(0.0, 1.0)]
[SecsParameter(id: 101, kind: SecsParameterKind.Recipe, dataType: "F8")]
public double Threshold
{
get => _threshold;
set => SetConfigProperty(ref _threshold, value);
}
}var builder = new ContainerBuilder();
builder.RegisterModule(new ConfigurationServiceModule(options =>
{
options.BaseFilePath = @"Config\base.json";
options.RecipeDirectory = @"Config\recipes";
options.EnableAudit = true;
options.EnableBackup = true;
}));
// 注册配置节点
builder.RegisterType<CameraConfig>()
.As<IBaseConfigure>()
.SingleInstance();
builder.RegisterType<InspectionAlgorithmConfig>()
.As<IRecipeConfigure>()
.SingleInstance();
var container = builder.Build();var recipe = container.Resolve<IRecipeConfiguration>();
// 切换配方(原地属性赋值,指针不变)
var result = await recipe.SwitchAsync("aoi-highsens-v1");
if (result.IsFailure)
logger.Error("切换失败: {Error}", result.Error);ConfigureBase ← Key(确定性 GUID)/ Name / Version / IsDirty / Clone / Validate
├── BaseConfigureBase ← 设备级标记(implements IBaseConfigure)
├── RecipeConfigureBase ← 配方级标记(implements IRecipeConfigure)
├── CompositeConfigureBase<T> ← 含 Children 集合(ICompositeConfigure)
└── ListConfigureBase<T> ← 列表容器(List<T> + ICompositeConfigure)
MVVM 变体(CNS.Core.Configuration.Mvvm):
└── BindableConfigureBase ← SetProperty → IsDirty = true → 发布 ConfigValueChanged 事件
└── BindableCompositeConfigureBase<T>
└── BindableListConfigureBase<T>
DI 容器注册时: container[InspectionAlgorithmConfig] → 0x1A2B3C (固定地址)
↑
配方切换时: RecipeConfiguration.SwitchAsync("recipe-b") │
└→ DeserializeAllAsync(filePath) │
└→ ApplySections(sections) │
└→ MapProperties(loaded, registered) │
└→ Mapster.Map(src → dst) ─────────┘
仅拷贝属性值,对象地址不变
// 订阅属性变更
eventBus.Subscribe<ConfigValueChangedEvent>(evt =>
{
logger.Info("{Property}: {Old} → {New}", evt.PropertyName, evt.OldValue, evt.NewValue);
});
// 批量操作(合并事件)
using (eventBus.BeginBatch())
{
config.Threshold = 0.3;
config.ExposureTime = 8000;
} // EndBatch 时统一触发一次public interface IConfigure : INamed, IKeyed, IVersioned
{
string Name { get; set; }
Guid Key { get; } // 确定性 GUID(由类型全名生成)
void RestoreDefault(object? param = null);
}public interface IConfigurationStore : IConfigureContainer
{
string FilePath { get; }
ConfigurationTier Tier { get; }
Task<RecordResult> SaveAsync(CancellationToken ct = default);
Task<RecordResult> LoadAsync(CancellationToken ct = default);
}public interface IRecipeConfiguration : IConfigurationStore
{
string CurrentRecipeId { get; }
RecipeMetadata CurrentMetadata { get; }
Task<RecordResult> SwitchAsync(string recipeId, CancellationToken ct = default);
Task<RecordResult> SaveAsAsync(string newRecipeId, RecipeMetadata metadata, CancellationToken ct = default);
}public interface IRecipeLibrary
{
IReadOnlyList<RecipeMetadata> AllRecipes { get; }
Task<RecordResult<string>> CreateAsync(RecipeMetadata metadata, IReadOnlyList<IConfigure> sections);
Task<RecordResult> UpdateMetadataAsync(string recipeId, Action<RecipeMetadata> update);
Task<RecordResult> DeleteAsync(string recipeId); // 软删除
Task<RecordResult> ImportAsync(string sourcePath);
Task<RecordResult> ExportAsync(string recipeId, string targetPath);
Task<RecordResult<IReadOnlyList<RecipeHistoryEntry>>> GetHistoryAsync(string recipeId);
}public interface IValidationPipeline
{
ValidationReport Validate(IConfigure section);
ValidationReport ValidateAll(IReadOnlyList<IConfigure> sections);
}
// ValidationLevel: Critical | Error | Warningcd demo/CNS.Core.Configuration.Demo
dotnet run| Tab | 演示内容 |
|---|---|
| System Config | JsonFileOperation 节/键读写、预设加载 |
| Base Config | 多设备配置 + PropertyGrid + 验证 + Undo/Redo + 快照差异 |
| Recipe Management | 配方库 CRUD / 复制 / 导入导出 / 历史 / 默认值恢复 |
| Security & Access | 访问控制 + 配置锁定 + 加密字段预览 |
| Audit & Diagnostics | 审计日志查询 + 健康报告 + 性能指标 |
| Event Bus & Batch | 事件订阅/发布 + 批量操作合并事件演示 |
| Format & Bootstrapper | XML/CSV/SECS 导出 + 双路备份 + 生命周期编排 |
Demo 内置 3 份 AOI 配方用于验证切换配方指针稳定性:
| 配方 ID | 配方名称 | Threshold | ExposureTime | 算法模式 |
|---|---|---|---|---|
aoi-standard-v1 |
AOI 标准版 | 0.5 | 5000 µs | PatternMatch |
aoi-highsens-v1 |
AOI 高灵敏度 | 0.3 | 8000 µs | DeepLearning+HDR |
aoi-lowpower-v1 |
AOI 低功耗 | 0.7 | 3000 µs | Edge |
切换配方后,在 UI 中观察三个配置对象的 RuntimeHelpers.GetHashCode() 值,验证地址不变。
配方文件为节数组,每个节包含 $type 鉴别符:
[
{
"$type": "CNS.Core.Configuration.Demo.Models.Recipe.InspectionAlgorithmConfig, CNS.Core.Configuration.Demo",
"Name": "检测算法配置",
"Version": "1.0.0",
"Threshold": 0.5,
"MinDefectSize": 5.0,
"SearchMode": 0
},
{
"$type": "CNS.Core.Configuration.Demo.Models.Recipe.CameraCaptureSettings, CNS.Core.Configuration.Demo",
"Name": "相机采集设置",
"ExposureTime": 5000,
"Gain": 1.0,
"EnableHDR": false
}
][
{
"RecipeId": "aoi-standard-v1",
"DisplayName": "AOI 标准版",
"Category": "AOI产品",
"Version": "1.0.0",
"IsDeleted": false,
"CreatedAt": "2025-01-01T00:00:00Z",
"FilePath": "AOI产品/AOI标准版.json"
}
]| 值 | 名称 | 说明 |
|---|---|---|
| 0 | Spiral | 螺旋扫描 |
| 1 | Raster | 光栅扫描 |
| 2 | Edge | 边缘扫描 |
// 声明敏感字段
[Sensitive]
public string ApiKey { get; set; } = string.Empty;
// 存储: "$enc:AES256:BASE64_ENCODED_VALUE"
// 访问控制
accessControl.EnsureCanModify("operator-001", ConfigurationTier.Recipe);
// → 仅 Engineer 和 Admin 可修改 Recipe 层
// 生产锁定
lockService.Lock("production-run-001");
// → 锁定期间任何 Set 操作均抛出 ConfigurationLockedException// 标注 SECS 参数
[SecsParameter(id: 101, kind: SecsParameterKind.Recipe, dataType: "F8",
description: "检测阈值", unit: "None")]
public double Threshold { get; set; }
// S2F13 — 查询参数
var param = secsRegistry.GetById(101);
// param.Value = 0.5, param.DataType = "F8"
// S2F15 — 设置参数
await secsRegistry.SetValueAsync(101, 0.3);
// 内部: 找到对应属性 → 验证范围 → 写入 → 触发 ConfigValueChanged通过 JSON 文件驱动依赖注入,无需修改代码即可切换实现:
{
"services": [
{
"type": "MyApp.Services.CameraService, MyApp",
"as": "MyApp.Abstractions.ICameraService",
"lifetime": "singleton",
"constructor": {
"deviceIndex": 0,
"baudRate": 9600
}
}
]
}builder.RegisterModule(new FileInjectionModule(@"Config\services.json"));# 运行全部测试
dotnet test
# 仅运行单元测试
dotnet test tests/CNS.Core.Configuration.Tests
# 仅运行 Autofac 集成测试
dotnet test tests/CNS.Core.Configuration.Autofac.Tests
# 生成覆盖率报告
dotnet test --collect:"XPlat Code Coverage"测试使用 xUnit 2.6.6 + FluentAssertions 6.12.0 + Moq / NSubstitute。
| 包 | 版本 | 用途 |
|---|---|---|
System.Text.Json |
内置 | JSON 序列化/反序列化 |
CompareNETObjects |
4.x | 深度对象变更检测 |
CSharpFunctionalExtensions |
2.x | Result / Maybe 错误处理 |
| 包 | 版本 | 用途 |
|---|---|---|
Autofac |
8.2.0 | DI 容器 |
Microsoft.Extensions.Configuration |
9.x | 配置文件读取 |
| 包 | 版本 | 用途 |
|---|---|---|
Avalonia |
11.2.3 | 跨平台 UI 框架 |
Avalonia.Controls.DataGrid |
11.2.3 | DataGrid 控件 |
Semi.Avalonia |
11.2.3 | UI 主题(Semi Design) |
Avalonia.PropertyGrid |
11.2.x | 属性编辑器控件 |
ReactiveUI |
20.x | MVVM 响应式框架 |
Mapster |
7.4.0 | 对象属性映射(配方切换) |
Autofac |
8.2.0 | 依赖注入容器 |
Serilog |
4.x | 结构化日志 |
详见 CHANGELOG.md。
本项目遵循以下约定:
- 目标框架:
net462;net8.0(所有库项目必须双目标) - 编译开关:
AllowUnsafeBlocks+ImplicitUsings+TreatWarningsAsErrors+GenerateDocumentationFile - 日志:Serilog,必须携带
DeviceId/RecipeId/TraceId - 异常:禁止空
catch {},必须向上抛出或转换为Result.Failure - 硬编码:禁止硬编码阈值参数,所有参数来自 Recipe / Config
- 硬件代码:必须提供 Mock / Simulation 模式
内部使用,版权归 CNS(深圳)视觉科技有限公司所有。