#0039 全局数据结构设计

This commit is contained in:
zhengxuan.zhang
2026-03-18 20:14:08 +08:00
parent c6144fae89
commit 67898edc3f
19 changed files with 1490 additions and 16 deletions
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FsCheck;
using XplorePlane.Models;
namespace XplorePlane.Tests.Generators
{
/// <summary>
/// FsCheck 自定义生成器:为配方相关模型定义 Arbitrary 生成器。
/// 使用 Arb.Register&lt;RecipeGenerators&gt;() 注册。
/// </summary>
public class RecipeGenerators
{
// ── PipelineModel: class with settable properties ──
public static Arbitrary<PipelineModel> PipelineModelArb()
{
var gen = from id in Arb.Default.Guid().Generator
from name in Arb.Default.NonEmptyString().Generator
from deviceId in Arb.Default.NonEmptyString().Generator
from ticks in Gen.Choose(0, int.MaxValue)
let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000)
let updatedAt = createdAt
select new PipelineModel
{
Id = id,
Name = name.Get,
DeviceId = deviceId.Get,
CreatedAt = createdAt,
UpdatedAt = updatedAt,
Nodes = new List<PipelineNodeModel>()
};
return Arb.From(gen);
}
// ── RecipeStep: reuses StateGenerators for hardware states ──
public static Arbitrary<RecipeStep> RecipeStepArb()
{
var gen = from stepIndex in Gen.Choose(0, 100)
from motion in StateGenerators.MotionStateArb().Generator
from ray in StateGenerators.RaySourceStateArb().Generator
from detector in StateGenerators.DetectorStateArb().Generator
from pipeline in PipelineModelArb().Generator
select new RecipeStep(stepIndex, motion, ray, detector, pipeline);
return Arb.From(gen);
}
// ── InspectionRecipe: 0-10 steps with sequential StepIndex ──
public static Arbitrary<InspectionRecipe> InspectionRecipeArb()
{
var gen = from id in Arb.Default.Guid().Generator
from name in Arb.Default.NonEmptyString().Generator
from ticks in Gen.Choose(0, int.MaxValue)
let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000)
let updatedAt = createdAt
from stepCount in Gen.Choose(0, 10)
from steps in GenSequentialSteps(stepCount)
select new InspectionRecipe(
id,
name.Get,
createdAt,
updatedAt,
steps.AsReadOnly());
return Arb.From(gen);
}
// ── RecipeExecutionState ──
public static Arbitrary<RecipeExecutionState> RecipeExecutionStateArb()
{
var gen = from totalSteps in Gen.Choose(0, 50)
from currentStep in Gen.Choose(0, Math.Max(totalSteps, 1) - 1)
from status in Gen.Elements(
RecipeExecutionStatus.Idle,
RecipeExecutionStatus.Running,
RecipeExecutionStatus.Paused,
RecipeExecutionStatus.Completed,
RecipeExecutionStatus.Error)
from recipeName in Arb.Default.NonEmptyString().Generator
select new RecipeExecutionState(currentStep, totalSteps, status, recipeName.Get);
return Arb.From(gen);
}
// ── Helper: generate a list of steps with sequential StepIndex 0..n-1 ──
private static Gen<List<RecipeStep>> GenSequentialSteps(int count)
{
if (count == 0)
return Gen.Constant(new List<RecipeStep>());
return Gen.Sequence(
Enumerable.Range(0, count).Select(i =>
from motion in StateGenerators.MotionStateArb().Generator
from ray in StateGenerators.RaySourceStateArb().Generator
from detector in StateGenerators.DetectorStateArb().Generator
from pipeline in PipelineModelArb().Generator
select new RecipeStep(i, motion, ray, detector, pipeline)
)
).Select(steps => steps.ToList());
}
}
}
@@ -0,0 +1,146 @@
using System;
using FsCheck;
using XplorePlane.Models;
namespace XplorePlane.Tests.Generators
{
/// <summary>
/// FsCheck 自定义生成器:为所有状态模型定义 Arbitrary 生成器。
/// 使用 Arb.Register&lt;StateGenerators&gt;() 注册。
/// </summary>
public class StateGenerators
{
// ── 位置范围: -10000 ~ 10000, 速度范围: 0 ~ 1000 ──
private static Gen<double> PositionGen =>
Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0);
private static Gen<double> SpeedGen =>
Gen.Choose(0, 1000000).Select(i => i / 1000.0);
private static Gen<double> VoltageGen =>
Gen.Choose(0, 450000).Select(i => i / 1000.0);
private static Gen<double> PowerGen =>
Gen.Choose(0, 3000000).Select(i => i / 1000.0);
private static Gen<double> FrameRateGen =>
Gen.Choose(0, 120000).Select(i => i / 1000.0);
private static Gen<double> MatrixGen =>
Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0);
// ── MotionState: 6 positions + 6 speeds ──
public static Arbitrary<MotionState> MotionStateArb()
{
var gen = from xm in PositionGen
from ym in PositionGen
from zt in PositionGen
from zd in PositionGen
from tiltD in PositionGen
from dist in PositionGen
from xmSpd in SpeedGen
from ymSpd in SpeedGen
from ztSpd in SpeedGen
from zdSpd in SpeedGen
from tiltDSpd in SpeedGen
from distSpd in SpeedGen
select new MotionState(xm, ym, zt, zd, tiltD, dist,
xmSpd, ymSpd, ztSpd, zdSpd, tiltDSpd, distSpd);
return Arb.From(gen);
}
// ── RaySourceState: bool + 2 doubles ──
public static Arbitrary<RaySourceState> RaySourceStateArb()
{
var gen = from isOn in Arb.Default.Bool().Generator
from voltage in VoltageGen
from power in PowerGen
select new RaySourceState(isOn, voltage, power);
return Arb.From(gen);
}
// ── DetectorState: 2 bools + double + string ──
public static Arbitrary<DetectorState> DetectorStateArb()
{
var gen = from isConnected in Arb.Default.Bool().Generator
from isAcquiring in Arb.Default.Bool().Generator
from frameRate in FrameRateGen
from res in Gen.Elements("1024x1024", "2048x2048", "2880x2880", "3072x3072", "4260x4260")
select new DetectorState(isConnected, isAcquiring, frameRate, res);
return Arb.From(gen);
}
// ── SystemState: OperationMode enum + bool + string ──
public static Arbitrary<SystemState> SystemStateArb()
{
var gen = from mode in Gen.Elements(
OperationMode.Idle,
OperationMode.Scanning,
OperationMode.CTAcquire,
OperationMode.RecipeRun)
from hasError in Arb.Default.Bool().Generator
from msg in Arb.Default.NonEmptyString().Generator
let errorMessage = hasError ? msg.Get : string.Empty
select new SystemState(mode, hasError, errorMessage);
return Arb.From(gen);
}
// ── CameraState: 2 bools + null CurrentFrame + 2 ints + double ──
public static Arbitrary<CameraState> CameraStateArb()
{
var gen = from isConnected in Arb.Default.Bool().Generator
from isStreaming in Arb.Default.Bool().Generator
from width in Gen.Choose(0, 8192)
from height in Gen.Choose(0, 8192)
from frameRate in FrameRateGen
select new CameraState(isConnected, isStreaming, null, width, height, frameRate);
return Arb.From(gen);
}
// ── CalibrationMatrix: 9 doubles ──
public static Arbitrary<CalibrationMatrix> CalibrationMatrixArb()
{
var gen = from m11 in MatrixGen
from m12 in MatrixGen
from m13 in MatrixGen
from m21 in MatrixGen
from m22 in MatrixGen
from m23 in MatrixGen
from m31 in MatrixGen
from m32 in MatrixGen
from m33 in MatrixGen
select new CalibrationMatrix(m11, m12, m13, m21, m22, m23, m31, m32, m33);
return Arb.From(gen);
}
// ── PhysicalPosition: 3 doubles ──
public static Arbitrary<PhysicalPosition> PhysicalPositionArb()
{
var gen = from x in PositionGen
from y in PositionGen
from z in PositionGen
select new PhysicalPosition(x, y, z);
return Arb.From(gen);
}
// ── LinkedViewState: PhysicalPosition + bool + DateTime ──
public static Arbitrary<LinkedViewState> LinkedViewStateArb()
{
var gen = from pos in PhysicalPositionArb().Generator
from isExecuting in Arb.Default.Bool().Generator
from ticks in Gen.Choose(0, int.MaxValue)
let dt = DateTime.MinValue.AddTicks((long)ticks * 10000)
select new LinkedViewState(pos, isExecuting, dt);
return Arb.From(gen);
}
}
}
@@ -0,0 +1,153 @@
using System;
using Xunit;
using XplorePlane.Models;
namespace XplorePlane.Tests.Models
{
public class StateModelsTests
{
// ── Default Value Tests ───────────────────────────────────────
[Fact]
public void MotionState_Default_AllZeros()
{
var state = MotionState.Default;
Assert.Equal(0, state.XM);
Assert.Equal(0, state.YM);
Assert.Equal(0, state.ZT);
Assert.Equal(0, state.ZD);
Assert.Equal(0, state.TiltD);
Assert.Equal(0, state.Dist);
Assert.Equal(0, state.XMSpeed);
Assert.Equal(0, state.YMSpeed);
Assert.Equal(0, state.ZTSpeed);
Assert.Equal(0, state.ZDSpeed);
Assert.Equal(0, state.TiltDSpeed);
Assert.Equal(0, state.DistSpeed);
}
[Fact]
public void RaySourceState_Default_IsOffAndZeros()
{
var state = RaySourceState.Default;
Assert.False(state.IsOn);
Assert.Equal(0, state.Voltage);
Assert.Equal(0, state.Power);
}
[Fact]
public void DetectorState_Default_DisconnectedAndZeros()
{
var state = DetectorState.Default;
Assert.False(state.IsConnected);
Assert.False(state.IsAcquiring);
Assert.Equal(0, state.FrameRate);
Assert.Equal(string.Empty, state.Resolution);
}
[Fact]
public void SystemState_Default_IdleNoError()
{
var state = SystemState.Default;
Assert.Equal(OperationMode.Idle, state.OperationMode);
Assert.False(state.HasError);
Assert.Equal(string.Empty, state.ErrorMessage);
}
[Fact]
public void CameraState_Default_DisconnectedAndZeros()
{
var state = CameraState.Default;
Assert.False(state.IsConnected);
Assert.False(state.IsStreaming);
Assert.Null(state.CurrentFrame);
Assert.Equal(0, state.Width);
Assert.Equal(0, state.Height);
Assert.Equal(0, state.FrameRate);
}
[Fact]
public void LinkedViewState_Default_ZeroPositionNotExecuting()
{
var state = LinkedViewState.Default;
Assert.Equal(0, state.TargetPosition.X);
Assert.Equal(0, state.TargetPosition.Y);
Assert.Equal(0, state.TargetPosition.Z);
Assert.False(state.IsExecuting);
Assert.Equal(DateTime.MinValue, state.LastRequestTime);
}
[Fact]
public void RecipeExecutionState_Default_IdleAndZeros()
{
var state = RecipeExecutionState.Default;
Assert.Equal(0, state.CurrentStepIndex);
Assert.Equal(0, state.TotalSteps);
Assert.Equal(RecipeExecutionStatus.Idle, state.Status);
Assert.Equal(string.Empty, state.CurrentRecipeName);
}
// ── Immutability Test ─────────────────────────────────────────
[Fact]
public void MotionState_WithExpression_ProducesNewInstance()
{
var original = MotionState.Default;
var modified = original with { XM = 100 };
// New instance is different from original
Assert.NotSame(original, modified);
Assert.Equal(100, modified.XM);
// Original is unchanged
Assert.Equal(0, original.XM);
}
// ── CalibrationMatrix Transform Tests ─────────────────────────
[Fact]
public void CalibrationMatrix_Transform_CorrectCalculation()
{
// Known matrix: scaling X by 2, Y by 3, with offsets
var matrix = new CalibrationMatrix(
M11: 2, M12: 0, M13: 10,
M21: 0, M22: 3, M23: 20,
M31: 0, M32: 0, M33: 0
);
var (x, y, z) = matrix.Transform(pixelX: 5, pixelY: 8);
// x = 2*5 + 0*8 + 10 = 20
Assert.Equal(20, x);
// y = 0*5 + 3*8 + 20 = 44
Assert.Equal(44, y);
// z = 0*5 + 0*8 + 0 = 0
Assert.Equal(0, z);
}
[Fact]
public void CalibrationMatrix_Transform_IdentityMatrix()
{
// Identity-like matrix: should return (pixelX, pixelY, 0) approximately
var identity = new CalibrationMatrix(
M11: 1, M12: 0, M13: 0,
M21: 0, M22: 1, M23: 0,
M31: 0, M32: 0, M33: 0
);
var (x, y, z) = identity.Transform(pixelX: 42.5, pixelY: 99.1);
Assert.Equal(42.5, x, precision: 10);
Assert.Equal(99.1, y, precision: 10);
Assert.Equal(0, z, precision: 10);
}
}
}
@@ -0,0 +1,121 @@
using System;
using System.Windows;
using Moq;
using Serilog;
using Xunit;
using XP.Hardware.RaySource.Services;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
namespace XplorePlane.Tests.Services
{
/// <summary>
/// AppStateService 单元测试。
/// 验证默认状态值、Dispose 后行为、null 参数校验、CalibrationMatrix 缺失时的错误处理。
/// </summary>
public class AppStateServiceTests : IDisposable
{
private readonly AppStateService _service;
private readonly Mock<IRaySourceService> _mockRaySource;
private readonly Mock<ILogger> _mockLogger;
public AppStateServiceTests()
{
// Ensure WPF Application exists for Dispatcher
if (Application.Current == null)
{
new Application();
}
_mockRaySource = new Mock<IRaySourceService>();
_mockLogger = new Mock<ILogger>();
_service = new AppStateService(_mockRaySource.Object, _mockLogger.Object);
}
public void Dispose()
{
_service.Dispose();
}
// ── 默认状态值验证 ──
[Fact]
public void DefaultState_MotionState_IsDefault()
{
Assert.Same(MotionState.Default, _service.MotionState);
}
[Fact]
public void DefaultState_RaySourceState_IsDefault()
{
Assert.Same(RaySourceState.Default, _service.RaySourceState);
}
[Fact]
public void DefaultState_SystemState_IsDefault()
{
Assert.Same(SystemState.Default, _service.SystemState);
}
[Fact]
public void DefaultState_CalibrationMatrix_IsNull()
{
Assert.Null(_service.CalibrationMatrix);
}
// ── null 参数抛出 ArgumentNullException ──
[Fact]
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateMotionState(null!));
}
[Fact]
public void UpdateRaySourceState_NullArgument_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateRaySourceState(null!));
}
[Fact]
public void UpdateDetectorState_NullArgument_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateDetectorState(null!));
}
[Fact]
public void UpdateSystemState_NullArgument_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateSystemState(null!));
}
// ── Dispose 后 Update 被忽略 ──
[Fact]
public void Dispose_ThenUpdate_IsIgnored()
{
var originalState = _service.MotionState;
_service.Dispose();
// Should not throw, and state should remain unchanged
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
_service.UpdateMotionState(newState);
Assert.Same(originalState, _service.MotionState);
}
// ── CalibrationMatrix 为 null 时 RequestLinkedView 设置错误状态 ──
[Fact]
public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState()
{
// CalibrationMatrix is null by default
Assert.Null(_service.CalibrationMatrix);
_service.RequestLinkedView(100.0, 200.0);
Assert.True(_service.SystemState.HasError);
Assert.NotEmpty(_service.SystemState.ErrorMessage);
}
}
}
@@ -0,0 +1,128 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Moq;
using Serilog;
using Xunit;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Recipe;
namespace XplorePlane.Tests.Services
{
/// <summary>
/// RecipeService 单元测试。
/// 验证配方创建、步骤记录、文件加载错误处理和名称校验。
/// </summary>
public class RecipeServiceTests
{
private readonly Mock<IAppStateService> _mockAppState;
private readonly Mock<IPipelineExecutionService> _mockPipeline;
private readonly Mock<ILogger> _mockLogger;
private readonly RecipeService _service;
public RecipeServiceTests()
{
_mockAppState = new Mock<IAppStateService>();
_mockPipeline = new Mock<IPipelineExecutionService>();
_mockLogger = new Mock<ILogger>();
// Setup default state returns
_mockAppState.Setup(s => s.MotionState).Returns(MotionState.Default);
_mockAppState.Setup(s => s.RaySourceState).Returns(RaySourceState.Default);
_mockAppState.Setup(s => s.DetectorState).Returns(DetectorState.Default);
_service = new RecipeService(
_mockAppState.Object,
_mockPipeline.Object,
_mockLogger.Object);
}
// ── CreateRecipe 验证 ──
[Fact]
public void CreateRecipe_ValidName_ReturnsEmptyRecipe()
{
var recipe = _service.CreateRecipe("TestRecipe");
Assert.Equal("TestRecipe", recipe.Name);
Assert.Empty(recipe.Steps);
Assert.NotEqual(Guid.Empty, recipe.Id);
Assert.True(recipe.CreatedAt <= DateTime.UtcNow);
Assert.True(recipe.UpdatedAt <= DateTime.UtcNow);
}
[Fact]
public void CreateRecipe_EmptyName_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(string.Empty));
}
[Fact]
public void CreateRecipe_NullName_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(null!));
}
[Fact]
public void CreateRecipe_WhitespaceName_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(" "));
}
// ── RecordCurrentStep 验证 ──
[Fact]
public void RecordCurrentStep_CapturesCurrentState()
{
var motionState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
var raySourceState = new RaySourceState(true, 120.0, 80.0);
var detectorState = new DetectorState(true, true, 30.0, "2048x2048");
_mockAppState.Setup(s => s.MotionState).Returns(motionState);
_mockAppState.Setup(s => s.RaySourceState).Returns(raySourceState);
_mockAppState.Setup(s => s.DetectorState).Returns(detectorState);
var recipe = _service.CreateRecipe("TestRecipe");
var pipeline = new PipelineModel { Name = "TestPipeline" };
var step = _service.RecordCurrentStep(recipe, pipeline);
Assert.Equal(0, step.StepIndex);
Assert.Same(motionState, step.MotionState);
Assert.Same(raySourceState, step.RaySourceState);
Assert.Same(detectorState, step.DetectorState);
Assert.Same(pipeline, step.Pipeline);
}
// ── LoadAsync 错误处理 ──
[Fact]
public async Task LoadAsync_FileNotExists_ThrowsFileNotFoundException()
{
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".json");
await Assert.ThrowsAsync<FileNotFoundException>(
() => _service.LoadAsync(nonExistentPath));
}
[Fact]
public async Task LoadAsync_InvalidJson_ThrowsInvalidDataException()
{
var tempFile = Path.GetTempFileName();
try
{
await File.WriteAllTextAsync(tempFile, "{ this is not valid json !!! }");
await Assert.ThrowsAsync<InvalidDataException>(
() => _service.LoadAsync(tempFile));
}
finally
{
if (File.Exists(tempFile))
File.Delete(tempFile);
}
}
}
}
+15 -14
View File
@@ -1,33 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>XplorePlane.Tests</RootNamespace>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<RootNamespace>XplorePlane.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FsCheck" Version="3.1.0" />
<PackageReference Include="FsCheck.Xunit" Version="3.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="xunit" Version="2.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
<PackageReference Include="FsCheck" Version="2.*" />
<PackageReference Include="FsCheck.Xunit" Version="2.*" />
<PackageReference Include="Moq" Version="4.*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
</ItemGroup>
<!-- 直接引用主项目依赖的本地 DLL -->
<ItemGroup>
<Reference Include="ImageProcessing.Core">
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
<Reference Include="XP.Hardware.RaySource">
<HintPath>..\XplorePlane\Libs\Hardware\XP.Hardware.RaySource.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
</Project>
+24
View File
@@ -6,6 +6,8 @@ using System.Windows;
using XplorePlane.Views;
using XplorePlane.ViewModels;
using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Recipe;
using Prism.Ioc;
using Prism.DryIoc;
using Prism.Modularity;
@@ -88,6 +90,22 @@ namespace XplorePlane
Log.Information("XplorePlane 应用程序退出");
Log.Information("========================================");
// 释放全局状态服务资源
try
{
var bootstrapper = AppBootstrapper.Instance;
if (bootstrapper != null)
{
var appStateService = bootstrapper.Container.Resolve<IAppStateService>();
appStateService?.Dispose();
Log.Information("全局状态服务资源已释放");
}
}
catch (Exception ex)
{
Log.Error(ex, "全局状态服务资源释放失败");
}
// 释放射线源资源
try
{
@@ -179,6 +197,12 @@ namespace XplorePlane
containerRegistry.RegisterSingleton<IPipelineExecutionService, PipelineExecutionService>();
containerRegistry.RegisterSingleton<IPipelinePersistenceService, PipelinePersistenceService>();
// 注册全局状态服务(单例)
containerRegistry.RegisterSingleton<IAppStateService, AppStateService>();
// 注册检测配方服务(单例)
containerRegistry.RegisterSingleton<IRecipeService, RecipeService>();
// 注册流水线 ViewModel(每次解析创建新实例)
containerRegistry.Register<PipelineEditorViewModel>();
containerRegistry.Register<OperatorToolboxViewModel>();
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+44
View File
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace XplorePlane.Models
{
/// <summary>检测配方中的单个步骤</summary>
public record RecipeStep(
int StepIndex,
MotionState MotionState,
RaySourceState RaySourceState,
DetectorState DetectorState,
PipelineModel Pipeline
);
/// <summary>检测配方(CNC 自动编程)</summary>
public record InspectionRecipe(
Guid Id,
string Name,
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<RecipeStep> Steps
)
{
/// <summary>追加步骤,返回新的 InspectionRecipe</summary>
public InspectionRecipe AddStep(RecipeStep step) =>
this with
{
Steps = Steps.Append(step).ToList().AsReadOnly(),
UpdatedAt = DateTime.UtcNow
};
}
/// <summary>配方执行状态(不可变)</summary>
public record RecipeExecutionState(
int CurrentStepIndex,
int TotalSteps,
RecipeExecutionStatus Status,
string CurrentRecipeName
)
{
public static readonly RecipeExecutionState Default = new(0, 0, RecipeExecutionStatus.Idle, string.Empty);
}
}
+120
View File
@@ -0,0 +1,120 @@
using System;
namespace XplorePlane.Models
{
// ── Enumerations ──────────────────────────────────────────────────
/// <summary>系统操作模式</summary>
public enum OperationMode
{
Idle, // 空闲
Scanning, // 扫描
CTAcquire, // CT 采集
RecipeRun // 配方执行中
}
/// <summary>配方执行状态</summary>
public enum RecipeExecutionStatus
{
Idle, // 空闲
Running, // 运行中
Paused, // 暂停
Completed, // 已完成
Error // 出错
}
// ── State Records ─────────────────────────────────────────────────
/// <summary>运动控制状态(不可变)</summary>
public record MotionState(
double XM, // X 轴位置 (μm)
double YM, // Y 轴位置 (μm)
double ZT, // Z 上轴位置 (μm)
double ZD, // Z 下轴位置 (μm)
double TiltD, // 倾斜角度 (m°)
double Dist, // 距离 (μm)
double XMSpeed, // X 轴速度 (μm/s)
double YMSpeed, // Y 轴速度 (μm/s)
double ZTSpeed, // Z 上轴速度 (μm/s)
double ZDSpeed, // Z 下轴速度 (μm/s)
double TiltDSpeed, // 倾斜速度 (m°/s)
double DistSpeed // 距离速度 (μm/s)
)
{
public static readonly MotionState Default = new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
/// <summary>射线源状态(不可变)</summary>
public record RaySourceState(
bool IsOn, // 开关状态
double Voltage, // 电压 (kV)
double Power // 功率 (W)
)
{
public static readonly RaySourceState Default = new(false, 0, 0);
}
/// <summary>探测器状态(不可变)</summary>
public record DetectorState(
bool IsConnected, // 连接状态
bool IsAcquiring, // 是否正在采集
double FrameRate, // 当前帧率 (fps)
string Resolution // 分辨率描述,如 "2048x2048"
)
{
public static readonly DetectorState Default = new(false, false, 0, string.Empty);
}
/// <summary>系统级状态(不可变)</summary>
public record SystemState(
OperationMode OperationMode, // 当前操作模式
bool HasError, // 是否存在系统错误
string ErrorMessage // 错误描述
)
{
public static readonly SystemState Default = new(OperationMode.Idle, false, string.Empty);
}
/// <summary>摄像头视频流状态(不可变)</summary>
public record CameraState(
bool IsConnected, // 连接状态
bool IsStreaming, // 是否正在推流
object CurrentFrame, // 当前帧数据引用(BitmapSource 或 byte[]Frozen
int Width, // 分辨率宽
int Height, // 分辨率高
double FrameRate // 帧率 (fps)
)
{
public static readonly CameraState Default = new(false, false, null, 0, 0, 0);
}
/// <summary>物理坐标</summary>
public record PhysicalPosition(double X, double Y, double Z);
/// <summary>图像标定矩阵,像素坐标 → 物理坐标映射</summary>
public record CalibrationMatrix(
double M11, double M12, double M13, // 3x3 仿射变换矩阵
double M21, double M22, double M23,
double M31, double M32, double M33
)
{
/// <summary>将像素坐标转换为物理坐标</summary>
public (double X, double Y, double Z) Transform(double pixelX, double pixelY)
{
double x = M11 * pixelX + M12 * pixelY + M13;
double y = M21 * pixelX + M22 * pixelY + M23;
double z = M31 * pixelX + M32 * pixelY + M33;
return (x, y, z);
}
}
/// <summary>画面联动状态(不可变)</summary>
public record LinkedViewState(
PhysicalPosition TargetPosition, // 目标物理坐标
bool IsExecuting, // 联动是否正在执行
DateTime LastRequestTime // 最近一次联动请求时间
)
{
public static readonly LinkedViewState Default = new(new PhysicalPosition(0, 0, 0), false, DateTime.MinValue);
}
}
@@ -0,0 +1,232 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using Prism.Mvvm;
using Serilog;
using XP.Hardware.RaySource.Services;
using XplorePlane.Models;
namespace XplorePlane.Services.AppState
{
/// <summary>
/// 全局应用状态管理服务实现。
/// 继承 BindableBase 以支持 WPF 数据绑定,使用 Interlocked.Exchange 保证线程安全写入,
/// 通过 Dispatcher.BeginInvoke 将事件调度到 UI 线程。
/// </summary>
public class AppStateService : BindableBase, IAppStateService
{
private readonly Dispatcher _dispatcher;
private readonly IRaySourceService _raySourceService;
private readonly ILogger _logger;
private bool _disposed;
// ── 状态字段(通过 Interlocked.Exchange 原子替换)──
private MotionState _motionState = MotionState.Default;
private RaySourceState _raySourceState = RaySourceState.Default;
private DetectorState _detectorState = DetectorState.Default;
private SystemState _systemState = SystemState.Default;
private CameraState _cameraState = CameraState.Default;
private CalibrationMatrix _calibrationMatrix;
private LinkedViewState _linkedViewState = LinkedViewState.Default;
private RecipeExecutionState _recipeExecutionState = RecipeExecutionState.Default;
// ── 类型化状态变更事件 ──
public event EventHandler<StateChangedEventArgs<MotionState>> MotionStateChanged;
public event EventHandler<StateChangedEventArgs<RaySourceState>> RaySourceStateChanged;
public event EventHandler<StateChangedEventArgs<DetectorState>> DetectorStateChanged;
public event EventHandler<StateChangedEventArgs<SystemState>> SystemStateChanged;
public event EventHandler<StateChangedEventArgs<CameraState>> CameraStateChanged;
public event EventHandler<StateChangedEventArgs<LinkedViewState>> LinkedViewStateChanged;
public event EventHandler<StateChangedEventArgs<RecipeExecutionState>> RecipeExecutionStateChanged;
public event EventHandler<LinkedViewRequestEventArgs> LinkedViewRequested;
// ── 状态属性(只读)──
public MotionState MotionState => _motionState;
public RaySourceState RaySourceState => _raySourceState;
public DetectorState DetectorState => _detectorState;
public SystemState SystemState => _systemState;
public CameraState CameraState => _cameraState;
public CalibrationMatrix CalibrationMatrix => _calibrationMatrix;
public LinkedViewState LinkedViewState => _linkedViewState;
public RecipeExecutionState RecipeExecutionState => _recipeExecutionState;
public AppStateService(
IRaySourceService raySourceService,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(raySourceService);
ArgumentNullException.ThrowIfNull(logger);
_raySourceService = raySourceService;
_logger = logger;
_dispatcher = Application.Current.Dispatcher;
SubscribeToExistingServices();
_logger.Information("AppStateService 已初始化");
}
// ── 状态更新方法 ──
public void UpdateMotionState(MotionState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateMotionState 调用"); return; }
var old = Interlocked.Exchange(ref _motionState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState));
}
public void UpdateRaySourceState(RaySourceState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateRaySourceState 调用"); return; }
var old = Interlocked.Exchange(ref _raySourceState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, RaySourceStateChanged, nameof(RaySourceState));
}
public void UpdateDetectorState(DetectorState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateDetectorState 调用"); return; }
var old = Interlocked.Exchange(ref _detectorState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, DetectorStateChanged, nameof(DetectorState));
}
public void UpdateSystemState(SystemState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateSystemState 调用"); return; }
var old = Interlocked.Exchange(ref _systemState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, SystemStateChanged, nameof(SystemState));
}
public void UpdateCameraState(CameraState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateCameraState 调用"); return; }
var old = Interlocked.Exchange(ref _cameraState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, CameraStateChanged, nameof(CameraState));
}
public void UpdateCalibrationMatrix(CalibrationMatrix newMatrix)
{
ArgumentNullException.ThrowIfNull(newMatrix);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateCalibrationMatrix 调用"); return; }
var old = Interlocked.Exchange(ref _calibrationMatrix, newMatrix);
if (ReferenceEquals(old, newMatrix)) return;
_dispatcher.BeginInvoke(() =>
{
RaisePropertyChanged(nameof(CalibrationMatrix));
});
}
public void UpdateLinkedViewState(LinkedViewState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateLinkedViewState 调用"); return; }
var old = Interlocked.Exchange(ref _linkedViewState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, LinkedViewStateChanged, nameof(LinkedViewState));
}
public void UpdateRecipeExecutionState(RecipeExecutionState newState)
{
ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warning("AppStateService 已释放,忽略 UpdateRecipeExecutionState 调用"); return; }
var old = Interlocked.Exchange(ref _recipeExecutionState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, RecipeExecutionStateChanged, nameof(RecipeExecutionState));
}
// ── 画面联动 ──
public void RequestLinkedView(double pixelX, double pixelY)
{
var matrix = _calibrationMatrix;
if (matrix is null)
{
_logger.Warning("CalibrationMatrix 未设置,无法执行画面联动 (pixelX={PixelX}, pixelY={PixelY})", pixelX, pixelY);
UpdateSystemState(SystemState with
{
HasError = true,
ErrorMessage = "CalibrationMatrix 未设置,无法执行画面联动"
});
return;
}
var (x, y, z) = matrix.Transform(pixelX, pixelY);
var newLinkedState = new LinkedViewState(
new PhysicalPosition(x, y, z),
IsExecuting: true,
LastRequestTime: DateTime.UtcNow);
UpdateLinkedViewState(newLinkedState);
_dispatcher.BeginInvoke(() =>
{
LinkedViewRequested?.Invoke(this, new LinkedViewRequestEventArgs(x, y, z));
});
}
// ── 内部辅助方法 ──
private void RaiseOnDispatcher<T>(T oldVal, T newVal,
EventHandler<StateChangedEventArgs<T>> handler, string propertyName)
{
_dispatcher.BeginInvoke(() =>
{
RaisePropertyChanged(propertyName);
try
{
handler?.Invoke(this, new StateChangedEventArgs<T>(oldVal, newVal));
}
catch (Exception ex)
{
_logger.Error(ex, "状态变更事件处理器抛出异常 (property={PropertyName})", propertyName);
}
});
}
private void SubscribeToExistingServices()
{
// IRaySourceService 是基于命令的服务(没有直接的状态变更事件),
// 状态同步由外部调用者通过 UpdateRaySourceState 完成。
// TODO: 当 IRaySourceService 添加状态变更事件时,在此处订阅并同步到 RaySourceState
_logger.Information("AppStateService 已准备好接收外部服务状态更新");
}
// ── Dispose ──
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 清除所有事件订阅
MotionStateChanged = null;
RaySourceStateChanged = null;
DetectorStateChanged = null;
SystemStateChanged = null;
CameraStateChanged = null;
LinkedViewStateChanged = null;
RecipeExecutionStateChanged = null;
LinkedViewRequested = null;
_logger.Information("AppStateService 已释放");
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.ComponentModel;
using XplorePlane.Models;
namespace XplorePlane.Services.AppState
{
/// <summary>
/// 全局应用状态管理服务接口。
/// 以单例模式注册,聚合所有硬件和系统状态,支持 WPF 数据绑定和类型化事件订阅。
/// </summary>
public interface IAppStateService : INotifyPropertyChanged, IDisposable
{
// ── 状态属性(只读,通过 Update 方法更新)──
MotionState MotionState { get; }
RaySourceState RaySourceState { get; }
DetectorState DetectorState { get; }
SystemState SystemState { get; }
CameraState CameraState { get; }
CalibrationMatrix CalibrationMatrix { get; }
LinkedViewState LinkedViewState { get; }
RecipeExecutionState RecipeExecutionState { get; }
// ── 状态更新方法(线程安全,可从任意线程调用)──
void UpdateMotionState(MotionState newState);
void UpdateRaySourceState(RaySourceState newState);
void UpdateDetectorState(DetectorState newState);
void UpdateSystemState(SystemState newState);
void UpdateCameraState(CameraState newState);
void UpdateCalibrationMatrix(CalibrationMatrix newMatrix);
void UpdateLinkedViewState(LinkedViewState newState);
void UpdateRecipeExecutionState(RecipeExecutionState newState);
// ── 画面联动 ──
/// <summary>将像素坐标通过 CalibrationMatrix 转换为物理坐标并发布 LinkedViewRequest</summary>
void RequestLinkedView(double pixelX, double pixelY);
// ── 类型化状态变更事件 ──
event EventHandler<StateChangedEventArgs<MotionState>> MotionStateChanged;
event EventHandler<StateChangedEventArgs<RaySourceState>> RaySourceStateChanged;
event EventHandler<StateChangedEventArgs<DetectorState>> DetectorStateChanged;
event EventHandler<StateChangedEventArgs<SystemState>> SystemStateChanged;
event EventHandler<StateChangedEventArgs<CameraState>> CameraStateChanged;
event EventHandler<StateChangedEventArgs<LinkedViewState>> LinkedViewStateChanged;
event EventHandler<StateChangedEventArgs<RecipeExecutionState>> RecipeExecutionStateChanged;
// ── 画面联动请求事件 ──
event EventHandler<LinkedViewRequestEventArgs> LinkedViewRequested;
}
}
@@ -0,0 +1,32 @@
using System;
namespace XplorePlane.Services.AppState
{
/// <summary>类型化状态变更事件参数,携带旧值和新值</summary>
public sealed class StateChangedEventArgs<T> : EventArgs
{
public T OldValue { get; }
public T NewValue { get; }
public StateChangedEventArgs(T oldValue, T newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}
/// <summary>画面联动请求事件参数,携带目标物理坐标</summary>
public sealed class LinkedViewRequestEventArgs : EventArgs
{
public double TargetX { get; }
public double TargetY { get; }
public double TargetZ { get; }
public LinkedViewRequestEventArgs(double targetX, double targetY, double targetZ)
{
TargetX = targetX;
TargetY = targetY;
TargetZ = targetZ;
}
}
}
@@ -0,0 +1,31 @@
using System.Threading;
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services.Recipe
{
/// <summary>检测配方管理服务接口,负责配方的创建、序列化/反序列化和执行编排</summary>
public interface IRecipeService
{
/// <summary>创建空的检测配方</summary>
InspectionRecipe CreateRecipe(string name);
/// <summary>从当前全局状态快照 + PipelineModel 记录一个步骤</summary>
RecipeStep RecordCurrentStep(InspectionRecipe recipe, PipelineModel pipeline);
/// <summary>序列化配方到 JSON 文件</summary>
Task SaveAsync(InspectionRecipe recipe, string filePath);
/// <summary>从 JSON 文件反序列化配方</summary>
Task<InspectionRecipe> LoadAsync(string filePath);
/// <summary>按步骤顺序执行配方</summary>
Task ExecuteAsync(InspectionRecipe recipe, CancellationToken cancellationToken = default);
/// <summary>暂停当前配方执行(当前步骤完成后停止)</summary>
void Pause();
/// <summary>恢复已暂停的配方执行</summary>
void Resume();
}
}
@@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
namespace XplorePlane.Services.Recipe
{
/// <summary>
/// 检测配方管理服务实现。
/// 负责配方的创建、序列化/反序列化和执行编排,通过 ManualResetEventSlim 支持暂停/恢复。
/// </summary>
public class RecipeService : IRecipeService
{
private readonly IAppStateService _appStateService;
private readonly IPipelineExecutionService _pipelineExecutionService;
private readonly ILogger _logger;
private readonly ManualResetEventSlim _pauseHandle = new(true); // initially signaled (not paused)
private static readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true
};
public RecipeService(
IAppStateService appStateService,
IPipelineExecutionService pipelineExecutionService,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(appStateService);
ArgumentNullException.ThrowIfNull(pipelineExecutionService);
ArgumentNullException.ThrowIfNull(logger);
_appStateService = appStateService;
_pipelineExecutionService = pipelineExecutionService;
_logger = logger;
_logger.Information("RecipeService 已初始化");
}
/// <inheritdoc />
public InspectionRecipe CreateRecipe(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("配方名称不能为空", nameof(name));
var recipe = new InspectionRecipe(
Id: Guid.NewGuid(),
Name: name,
CreatedAt: DateTime.UtcNow,
UpdatedAt: DateTime.UtcNow,
Steps: new List<RecipeStep>().AsReadOnly());
_logger.Information("已创建配方: {RecipeName}, Id={RecipeId}", name, recipe.Id);
return recipe;
}
/// <inheritdoc />
public RecipeStep RecordCurrentStep(InspectionRecipe recipe, PipelineModel pipeline)
{
ArgumentNullException.ThrowIfNull(recipe);
ArgumentNullException.ThrowIfNull(pipeline);
var motionState = _appStateService.MotionState;
var raySourceState = _appStateService.RaySourceState;
var detectorState = _appStateService.DetectorState;
var step = new RecipeStep(
StepIndex: recipe.Steps.Count,
MotionState: motionState,
RaySourceState: raySourceState,
DetectorState: detectorState,
Pipeline: pipeline);
_logger.Information("已记录配方步骤: StepIndex={StepIndex}, Recipe={RecipeName}",
step.StepIndex, recipe.Name);
return step;
}
/// <inheritdoc />
public async Task SaveAsync(InspectionRecipe recipe, string filePath)
{
ArgumentNullException.ThrowIfNull(recipe);
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证:检查路径遍历
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException($"文件路径包含不安全的路径遍历字符: {filePath}");
var json = JsonSerializer.Serialize(recipe, _jsonOptions);
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
_logger.Information("已保存配方到文件: {FilePath}, Recipe={RecipeName}", filePath, recipe.Name);
}
/// <inheritdoc />
public async Task<InspectionRecipe> LoadAsync(string filePath)
{
ArgumentNullException.ThrowIfNull(filePath);
if (!File.Exists(filePath))
throw new FileNotFoundException($"配方文件不存在: {filePath}", filePath);
string json;
try
{
json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not FileNotFoundException)
{
throw new InvalidDataException($"无法读取配方文件: {filePath}", ex);
}
InspectionRecipe recipe;
try
{
recipe = JsonSerializer.Deserialize<InspectionRecipe>(json, _jsonOptions);
}
catch (JsonException ex)
{
throw new InvalidDataException($"配方文件 JSON 格式无效: {filePath}", ex);
}
if (recipe is null)
throw new InvalidDataException($"配方文件反序列化结果为 null: {filePath}");
if (string.IsNullOrWhiteSpace(recipe.Name))
throw new InvalidDataException($"配方文件数据不完整,缺少名称: {filePath}");
if (recipe.Steps is null)
throw new InvalidDataException($"配方文件数据不完整,缺少步骤列表: {filePath}");
_logger.Information("已加载配方: {FilePath}, Recipe={RecipeName}, Steps={StepCount}",
filePath, recipe.Name, recipe.Steps.Count);
return recipe;
}
/// <inheritdoc />
public async Task ExecuteAsync(InspectionRecipe recipe, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(recipe);
_logger.Information("开始执行配方: {RecipeName}, 共 {StepCount} 步", recipe.Name, recipe.Steps.Count);
// 重置暂停状态
_pauseHandle.Set();
_appStateService.UpdateRecipeExecutionState(new RecipeExecutionState(
CurrentStepIndex: 0,
TotalSteps: recipe.Steps.Count,
Status: RecipeExecutionStatus.Running,
CurrentRecipeName: recipe.Name));
try
{
for (int i = 0; i < recipe.Steps.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
// 暂停检查:等待恢复信号
_pauseHandle.Wait(cancellationToken);
var step = recipe.Steps[i];
_appStateService.UpdateRecipeExecutionState(new RecipeExecutionState(
CurrentStepIndex: i,
TotalSteps: recipe.Steps.Count,
Status: RecipeExecutionStatus.Running,
CurrentRecipeName: recipe.Name));
_logger.Information("执行配方步骤 {StepIndex}/{TotalSteps}: Recipe={RecipeName}",
i + 1, recipe.Steps.Count, recipe.Name);
try
{
await ExecuteStepAsync(step, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw; // 让取消异常向上传播
}
catch (Exception ex)
{
_logger.Error(ex, "配方步骤 {StepIndex} 执行失败: Recipe={RecipeName}", i, recipe.Name);
_appStateService.UpdateRecipeExecutionState(new RecipeExecutionState(
CurrentStepIndex: i,
TotalSteps: recipe.Steps.Count,
Status: RecipeExecutionStatus.Error,
CurrentRecipeName: recipe.Name));
_appStateService.UpdateSystemState(new SystemState(
OperationMode: OperationMode.Idle,
HasError: true,
ErrorMessage: $"配方 '{recipe.Name}' 步骤 {i} 执行失败: {ex.Message}"));
return;
}
}
_appStateService.UpdateRecipeExecutionState(new RecipeExecutionState(
CurrentStepIndex: recipe.Steps.Count,
TotalSteps: recipe.Steps.Count,
Status: RecipeExecutionStatus.Completed,
CurrentRecipeName: recipe.Name));
_logger.Information("配方执行完成: {RecipeName}", recipe.Name);
}
catch (OperationCanceledException)
{
_logger.Information("配方执行已取消: {RecipeName}", recipe.Name);
_appStateService.UpdateRecipeExecutionState(new RecipeExecutionState(
CurrentStepIndex: 0,
TotalSteps: recipe.Steps.Count,
Status: RecipeExecutionStatus.Idle,
CurrentRecipeName: recipe.Name));
throw;
}
}
/// <inheritdoc />
public void Pause()
{
_pauseHandle.Reset();
var current = _appStateService.RecipeExecutionState;
if (current.Status == RecipeExecutionStatus.Running)
{
_appStateService.UpdateRecipeExecutionState(current with
{
Status = RecipeExecutionStatus.Paused
});
}
_logger.Information("配方执行已暂停");
}
/// <inheritdoc />
public void Resume()
{
var current = _appStateService.RecipeExecutionState;
if (current.Status == RecipeExecutionStatus.Paused)
{
_appStateService.UpdateRecipeExecutionState(current with
{
Status = RecipeExecutionStatus.Running
});
}
_pauseHandle.Set();
_logger.Information("配方执行已恢复");
}
// ── 内部辅助方法 ──
private async Task ExecuteStepAsync(RecipeStep step, CancellationToken cancellationToken)
{
// 更新运动状态
_appStateService.UpdateMotionState(step.MotionState);
// 更新射线源状态
_appStateService.UpdateRaySourceState(step.RaySourceState);
// 更新探测器状态
_appStateService.UpdateDetectorState(step.DetectorState);
// TODO: 通过 IPipelineExecutionService 执行 Pipeline
// IPipelineExecutionService.ExecutePipelineAsync 需要 IEnumerable<PipelineNodeViewModel> 和 BitmapSource
// 而 RecipeStep 持有 PipelineModel。此处记录日志作为占位,待 ViewModel 层适配后完善。
_logger.Information("步骤 {StepIndex}: 已更新设备状态,Pipeline '{PipelineName}' 执行待实现",
step.StepIndex, step.Pipeline?.Name ?? "N/A");
await Task.CompletedTask;
}
}
}
+3 -2
View File
@@ -34,8 +34,9 @@
2026.3.18
----------------------
1、全局数据结构的考虑与设计
2、将计划窗体默认隐藏,只有CNC状态下展开
1、全局数据结构的考虑与设计(多个窗体可以调用公共的数据,如射线源状态,探测器状态,运动位置,图像等)
2、将计划窗体默认隐藏,只有CNC状态下展开
3、日志该用XP.Common库和多语言的学习