From 8759b831b26c180b59616fb8247786bddf2bc888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noah=20Choi=20=28=EC=B5=9C=EB=85=B8=EC=95=84=29?= <73541496+Noah8218@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:23:27 +0900 Subject: [PATCH] chore(release): promote v0.2.0-dev.20 folder organization Promote the responsibility-folder refactor and shutdown diagnostics to the public repository. Preserve namespaces, bindings, runtime contracts, and deterministic simulation behavior. Version: v0.2.0-dev.20. --- Directory.Build.props | 2 +- OpenVisionLab.MachineStudio.sln | 9 + README.md | 13 +- docs/ARCHITECTURE.md | 35 +- docs/NET_VERSION_POLICY.md | 2 +- .../Projects/ProjectDocumentStore.cs | 100 +--- .../Exchange/MachineIntegrationExchange.cs | 92 +++ .../MachineIntegrationHandoffPublisher.cs | 0 ...MachineIntegrationHandoffRequestFactory.cs | 69 ++- .../MachineIntegrationLifecycle.cs | 0 .../MachineIntegrationPathReadinessPolicy.cs | 6 +- .../MachineIntegrationResultFileWatcher.cs | 12 +- ...ineIntegrationResultObservationWorkflow.cs | 5 +- .../Exchange}/MachineIntegrationSetupStore.cs | 17 +- .../MachineIntegrationTcpExchange.cs | 0 ...MachineLocatorIntegrationRecipeContract.cs | 0 .../MachineCoordinateProjectionContract.cs | 315 ---------- .../MachineCoordinateProjectionContract.cs | 156 +++++ .../MachineCoordinateProjectionMapper.cs | 50 ++ .../MachineCoordinateProjectionPersistence.cs | 51 ++ .../MachineCoordinateProjectionValidator.cs | 122 ++++ ...MachineIntegrationTransactionFileSystem.cs | 0 ...MachineIntegrationTransactionInspector.cs} | 332 ++++------- ...MachineIntegrationTransactionPublisher.cs} | 149 +---- .../MachineIntegrationTransactionRetention.cs | 230 ++++++++ .../OpenVisionLab.Machine.Persistence.csproj | 13 + .../Projects/ProjectDocumentFileCatalog.cs | 40 ++ .../Projects/ProjectDocumentFileStore.cs | 152 +++++ .../Runtime/DeterministicSequenceExecutor.cs | 4 +- .../{ => Axis}/ConfigureAxesCommand.cs | 0 .../Commands/{ => Axis}/HomeAxisCommand.cs | 0 .../Commands/{ => Axis}/JogAxisCommand.cs | 0 .../{ => Axis}/MoveAbsoluteCommand.cs | 0 .../{ => Axis}/MoveRelativeCommand.cs | 0 .../{ => Axis}/MoveVelocityCommand.cs | 0 .../Commands/{ => Axis}/StopAxesCommand.cs | 0 .../Commands/{ => Axis}/StopAxisCommand.cs | 0 .../TriggerVirtualCameraCommand.cs | 0 .../{ => Equipment}/SetConveyorCommand.cs | 0 .../{ => Equipment}/SetCylinderCommand.cs | 0 .../SetDigitalSensorForceCommand.cs | 0 .../{ => Equipment}/SetVirtualInputCommand.cs | 0 .../SetVirtualInputForceCommand.cs | 0 .../ClearSimulationFaultCommand.cs | 0 .../InjectSimulationFaultCommand.cs | 0 .../ConfigureRuntimeCommand.cs | 0 .../MoveAxesAbsoluteCommand.cs | 0 .../Commands/{ => RunControl}/PauseCommand.cs | 0 .../Commands/{ => RunControl}/PlayCommand.cs | 0 .../Commands/{ => RunControl}/ResetCommand.cs | 0 .../{ => RunControl}/SimulationCommand.cs | 0 .../SimulationCommandResult.cs | 0 .../StartAutomaticRunCommand.cs | 0 .../StartConditionScenarioCommand.cs | 0 .../StartManualControlCommand.cs | 0 .../Commands/{ => RunControl}/StepCommand.cs | 0 .../StopConditionScenarioCommand.cs | 0 .../{ => Sequence}/AbortSequenceCommand.cs | 0 .../{ => Sequence}/RetrySequenceCommand.cs | 0 .../SetSequenceBreakpointCommand.cs | 0 .../{ => Sequence}/StartSequenceCommand.cs | 0 .../{ => Sequence}/StepSequenceCommand.cs | 0 .../AutomaticRunConfiguration.cs | 0 .../SimulationAutomaticRunCommandHandler.cs | 0 .../SimulationAutomaticRunCycleHandler.cs | 0 .../SimulationAutomaticRunRuntime.cs | 0 ...mulationConditionScenarioCommandHandler.cs | 0 ...ulationConditionScenarioProgressHandler.cs | 0 .../SimulationConditionScenarioRuntime.cs | 0 .../SimulationConditionScenarioStopHandler.cs | 0 ...ConditionScheduledFaultInjectionHandler.cs | 0 ...nConditionScheduledFaultRecoveryHandler.cs | 0 .../SimulationEngineFaultPoint.cs | 0 .../SimulationFaultCommandHandler.cs | 0 .../{ => Faults}/SimulationFaultRuntime.cs | 0 .../SimulationManualAxisCommandHandler.cs | 0 .../SimulationManualCameraCommandHandler.cs | 0 .../SimulationManualControlCommandHandler.cs | 0 ...SimulationManualEquipmentCommandHandler.cs | 0 .../SimulationManualInputCommandHandler.cs | 0 .../SimulationPhysicalRuntimeTick.cs | 0 ...eterministicSimulationCommandTraceStore.cs | 0 .../FixedStepSimulationEngine.cs | 4 + .../Engine/{ => Runtime}/ISimulationEngine.cs | 0 .../Engine/{ => Runtime}/SimulationClock.cs | 0 .../{ => Runtime}/SimulationControlOwner.cs | 0 .../SimulationEngineLifecycle.cs | 0 .../SimulationEngineTermination.cs | 0 .../SimulationRunControlCommandHandler.cs | 0 .../{ => Runtime}/SimulationRunLoopTiming.cs | 0 .../Engine/{ => Runtime}/SimulationRunMode.cs | 0 .../SimulationRuntimeConfiguration.cs | 0 .../SimulationRuntimeConfigurationBuilder.cs | 0 .../{ => Runtime}/SimulationRuntimeState.cs | 0 .../{ => Runtime}/SimulationSettings.cs | 0 .../DeterministicSequenceRuntimeContext.cs | 0 .../SimulationSequenceCommandHandler.cs | 0 .../SimulationSequenceRuntime.cs | 0 ...eterministicFaultScenarioHeadlessRunner.cs | 6 +- .../DeterministicFaultScenarioProfile.cs | 8 +- ...DeterministicScenarioAssertionEvaluator.cs | 0 .../DeterministicScenarioAssertions.cs | 0 ...erministicSimulationBatchResultComparer.cs | 0 ...terministicSimulationBatchResultPackage.cs | 0 .../DeterministicSimulationBatchRunner.cs | 0 .../DeterministicSimulationCommandTrace.cs | 0 ...isticSimulationCommandTraceCommandCodec.cs | 0 ...isticSimulationCommandTraceReplayRunner.cs | 0 ...sticUnifiedCommissioningEvidencePackage.cs | 0 .../DeterministicConditionScenarioProfile.cs | 7 +- ...erministicConditionScenarioReplayResult.cs | 0 .../DeterministicConditionScenarioRunner.cs | 0 .../DeterministicConditionScenarioRuntime.cs | 0 .../{ => Evidence}/AtomicEvidenceFile.cs | 23 +- ...nisticSimulationEvidenceExchangePackage.cs | 0 ...eterministicSimulationRunEvidenceHasher.cs | 0 ...eterministicSimulationRunResultComparer.cs | 0 ...DeterministicSimulationRunResultPackage.cs | 0 .../SimulationEvidenceRetentionPolicy.cs | 0 ...rministicVisionExecutionEvidencePackage.cs | 113 ---- .../DeterministicVisionExecutionRecorder.cs | 122 ++++ .../OpenVisionLab.MachineStudio.csproj | 1 + .../SmokeCameraCommissioningVerifier.cs | 0 .../SmokeCameraFirstUseVerifier.cs | 4 +- .../SmokeAnalogIoAuthoringVerifier.cs | 0 .../SmokeAxisCommissioningStateVerifier.cs | 0 .../SmokeAxisCommissioningVerifier.cs | 0 .../SmokeAxisTuningStateVerifier.cs | 0 .../SmokeConveyorCommissioningVerifier.cs | 0 .../SmokeCylinderCommissioningVerifier.cs | 0 .../SmokeDigitalIoCommissioningVerifier.cs | 0 .../SmokeLoadLockSetupVerifier.cs | 0 ...mokeMultiAxisCommissioningStateVerifier.cs | 0 .../SmokeMultiAxisCommissioningVerifier.cs | 0 .../SmokeSensorCommissioningVerifier.cs | 0 .../DirectExeConnectionWorkbenchWorkflow.cs | 0 .../{ => Host}/DirectExeFaultScenarioHost.cs | 0 .../DirectExeSmokeArgumentParser.cs | 0 .../{ => Host}/DirectExeSmokeFailurePolicy.cs | 0 .../Smoke/{ => Host}/DirectExeSmokeHost.cs | 8 + .../Smoke/{ => Host}/DirectExeSmokeOptions.cs | 0 .../{ => Host}/DirectExeSmokeProjectLoader.cs | 0 .../{ => Host}/MachineIntegrationExeSmoke.cs | 26 +- ...achineIntegrationExeSmokeCameraScenario.cs | 0 .../{ => Host}/SmokePerformanceVerifier.cs | 0 .../SmokeConnectionWorkbenchVerifier.cs | 3 +- .../SmokeIntegrationResultVerifier.cs | 6 +- .../SmokeCanvasNavigationVerifier.cs | 0 .../SmokeDirectSceneAuthoringVerifier.cs | 0 .../SmokeDirectTransformVerifier.cs | 0 .../SmokeEditMenuStateVerifier.cs | 0 .../SmokeEvidenceDrawerStateVerifier.cs | 0 .../SmokeGlobalCommandStateVerifier.cs | 0 .../{ => Layout}/SmokeLayerOrderVerifier.cs | 0 .../SmokeLayoutAlignmentVerifier.cs | 0 .../SmokeLayoutHistoryVerifier.cs | 3 +- .../SmokeLayoutPropertyStateVerifier.cs | 0 .../{ => Layout}/SmokeLibraryDropVerifier.cs | 0 .../SmokeMultiSelectionTransformVerifier.cs | 0 .../SmokeProjectRoundTripVerifier.cs | 0 .../SmokeProjectSafetyVerifier.cs | 126 ++++ .../{ => Project}/SmokeProjectTreeQuery.cs | 0 .../SmokeBatchPersistenceVerifier.cs | 0 .../SmokePickAndPlaceStateVerifier.cs | 0 .../SmokeProcessBlockApplicationVerifier.cs | 0 .../SmokeProcessBlockEditVerifier.cs | 0 .../SmokeProcessBlockPreparation.cs | 0 .../SmokeProcessBlockProposalVerifier.cs | 0 .../SmokeProcessBlockSequenceStateVerifier.cs | 0 .../SmokeProcessBlockStepStatusVerifier.cs | 0 .../SmokeProcessBlockTimeoutVerifier.cs | 0 .../SmokeRecipeCheckpointStateVerifier.cs | 0 .../SmokeRecipeConnectionStateVerifier.cs | 0 .../SmokeRecipeDryRunStateVerifier.cs | 11 +- .../SmokeRecipeGalleryVerifier.cs | 3 +- .../SmokeSemanticSetupVerifier.cs | 113 ++-- .../SmokeStationSkeletonVerifier.cs | 0 .../SmokeFaultManagerVerifier.cs | 0 .../SmokeRuntimeDebuggerVerifier.cs | 0 .../SmokeRuntimeEvidenceVerifier.cs | 0 .../SmokeScenarioBatchVerifier.cs | 0 .../SmokeSequenceStateVerifier.cs | 0 .../SmokeTestScenarioRuntimeVerifier.cs | 0 .../{ => Support}/SmokeButtonPointerState.cs | 0 .../Smoke/{ => Support}/SmokeNativeInput.cs | 0 .../{ => Support}/SmokeRoundTripScenario.cs | 0 .../Smoke/{ => Support}/SmokeUiInteraction.cs | 0 .../{ => Support}/SmokeVisualTreeQuery.cs | 0 .../Smoke/{ => Support}/SmokeWindowCapture.cs | 0 .../{ => Support}/SmokeWorkflowReport.cs | 0 .../View/Inspector/RightToolRegionView.xaml | 34 +- .../Project/RecipeConnectionQuickAddView.xaml | 10 +- .../RecipeConnectionWorkbenchView.xaml | 110 ++-- .../View/Shell/MainWpfInteractionHost.cs | 55 +- .../MultiAxisCommissioningViewModel.cs | 64 ++- .../MachineIntegrationRequestWorkflow.cs | 6 +- .../MachineIntegrationSetupViewModel.cs | 424 ++++++++++++++ .../MachineIntegrationTcpControlViewModel.cs | 45 +- .../MachineIntegrationViewModel.cs | 441 ++------------ .../ViewModel/MainViewModel.cs | 2 +- .../Project/ProjectLifecycleCoordinator.cs | 10 +- .../ViewModel/Project/ProjectOpenWorkflow.cs | 5 +- .../ViewModel/Project/ProjectSaveWorkflow.cs | 10 +- .../ProcessPlanReviewViewModel.cs | 0 .../RecipeAuthoringWorkspace.cs | 0 .../RecipeCheckpointTemplateViewModel.cs | 0 .../RecipeProcessBlockViewModel.cs | 0 .../RecipeConnectionProjectApplier.cs | 0 .../RecipeConnectionRowCatalog.cs | 0 .../RecipeConnectionSetupWorkflow.cs | 0 .../RecipeConnectionSimulationWorkflow.cs | 0 .../RecipeConnectionWorkbenchViewModel.cs | 0 .../RecipeDryRunPlaybackViewModel.cs | 0 .../{ => DryRun}/RecipeDryRunViewModel.cs | 0 .../RecipeSequenceStepPreviewViewModel.cs | 0 ...ackCompatibilityComparisonItemViewModel.cs | 0 .../SemiconductorRecipeCopyWorkflow.cs | 5 +- .../SemiconductorRecipeGalleryCatalog.cs | 14 +- ...SemiconductorRecipeGalleryItemViewModel.cs | 0 ...conductorRecipeGalleryValidationSession.cs | 0 ...onductorRecipeGalleryValidationWorkflow.cs | 3 +- .../SemiconductorRecipeGalleryViewModel.cs | 0 .../InspectionHandoffSetupViewModel.cs | 209 +++++++ .../InspectionSortRouterSetupViewModel.cs | 202 +++++++ .../Semantic/OhtHandoffSetupViewModel.cs | 215 +++++++ .../Semantic/PrealignerSetupViewModel.cs | 241 ++++++++ .../SemanticEquipmentSetupViewModel.cs | 94 +++ .../Semantic/WaferHandlerSetupViewModel.cs | 270 +++++++++ .../SemanticEquipmentSetupViewModel.cs | 541 ------------------ .../{ => Station}/LoadLockSetupViewModel.cs | 0 .../StationSkeletonSetupViewModel.cs | 0 .../{ => Faults}/FaultManagerViewModel.cs | 0 .../ScheduledFaultScenarioWorkflow.cs | 0 .../BoundedShutdownCoordinator.cs | 0 .../RuntimeAlarmCollectionViewModel.cs | 0 .../{ => Runtime}/RuntimeDebuggerViewModel.cs | 0 .../RuntimeDebuggerWatchTargetCatalog.cs | 0 .../RuntimeDefinitionApplicationWorkflow.cs | 0 .../RuntimeObservabilityJournal.cs | 0 .../RuntimeObservabilityPresenter.cs | 0 .../{ => Runtime}/RuntimeTimelineViewModel.cs | 0 .../SimulationRunControlAdmissionPolicy.cs | 0 .../SimulationRunControlWorkflow.cs | 0 .../{ => Runtime}/SimulationRuntimeLoop.cs | 2 +- .../SimulationRuntimeProjectionCoordinator.cs | 0 .../SimulationRuntimeResourceOwner.cs | 0 .../SimulationRuntimeShutdownWorkflow.cs | 51 +- .../SimulationRuntimeSnapshotProjection.cs | 0 .../SimulationScenarioBatchArtifactStore.cs | 0 .../SimulationScenarioBatchParticipant.cs | 0 .../SimulationScenarioBatchPresentation.cs | 0 ...SimulationScenarioBatchRepetitionRunner.cs | 0 .../SimulationScenarioBatchViewModel.cs | 72 ++- .../SimulationScenarioExecutionCoordinator.cs | 0 .../SimulationScenarioWorkflow.cs | 0 .../SimulationSessionCloseWorkflow.cs | 0 .../SimulationSessionCoordinator.cs | 3 +- .../SimulationWorkspaceViewModel.cs | 0 ...SimulationCommandPresentationDispatcher.cs | 0 .../SimulationCommandTraceViewModel.cs | 0 .../OpenVisionLab.Machine.Core.Tests.csproj | 1 + .../ProjectDocumentStoreTests.cs | 34 +- ...neIntegrationHandoffRequestFactoryTests.cs | 67 +-- .../{ => Axis}/ServoAxisComponentTests.cs | 0 .../TrapezoidalMotionProfileTests.cs | 0 .../DeterministicVirtualCameraTests.cs | 0 .../VirtualCameraInspectionTemplateTests.cs | 0 ...mulationAutomaticRunCommandHandlerTests.cs | 0 ...ionConditionScenarioCommandHandlerTests.cs | 0 ...onConditionScenarioProgressHandlerTests.cs | 0 ...lationConditionScenarioStopHandlerTests.cs | 0 ...tionScheduledFaultInjectionHandlerTests.cs | 0 ...itionScheduledFaultRecoveryHandlerTests.cs | 0 .../SimulationFaultCommandHandlerTests.cs | 0 ...ulationManualControlCommandHandlerTests.cs | 0 ...SimulationRunControlCommandHandlerTests.cs | 0 .../SimulationSequenceCommandHandlerTests.cs | 0 .../MultiAxisCommissioningRecipeTests.cs | 0 .../FixedStepDelayConverterTests.cs | 0 ...eLayoutWaferHandlerRuntimeCompilerTests.cs | 0 .../MachineProjectRuntimeCompilerTests.cs | 0 ...ineProjectRuntimeComponentCompilerTests.cs | 0 .../PrealignerCompilerTests.cs | 0 ...esentativeRecipeCheckpointTemplateTests.cs | 0 .../SemiconductorProcessBlockComposerTests.cs | 0 ...miconductorStationSkeletonTemplateTests.cs | 0 .../SequenceCompilerTests.cs | 0 .../{ => Evidence}/AtomicEvidenceFileTests.cs | 24 + .../ConcreteFaultRepeatComparisonTests.cs | 0 ...cSimulationEvidenceExchangePackageTests.cs | 0 ...nifiedCommissioningEvidencePackageTests.cs | 0 ...sticVisionExecutionEvidencePackageTests.cs | 0 .../PickAndPlaceFaultRecoveryEvidenceTests.cs | 0 .../{ => Evidence}/ScaleEvidenceTests.cs | 0 .../SimulationEvidenceRetentionPolicyTests.cs | 0 .../SimulationOutputStoreTests.cs | 0 .../SustainedScaleEvidenceTests.cs | 0 ...TransferCellScheduledFaultRecoveryTests.cs | 0 .../DeterministicConditionScenarioTests.cs | 1 + .../DeterministicFaultInjectionTests.cs | 0 ...inisticFaultScenarioHeadlessRunnerTests.cs | 0 .../DeterministicFaultScenarioProfileTests.cs | 36 ++ .../DeterministicFaultScenarioReplayTests.cs | 0 ...SimulationConditionScenarioRuntimeTests.cs | 0 .../SimulationFaultTargetCatalogTests.cs | 0 .../AutomaticRunIntegrationTests.cs | 0 .../AutomaticTransferCellEndToEndTests.cs | 0 ...chineLayoutAutomaticRunIntegrationTests.cs | 0 .../PickAndPlaceSampleEndToEndTests.cs | 0 .../SemiconductorRecipeEndToEndTests.cs | 0 .../SubsequenceCompositionTests.cs | 35 ++ .../VirtualCameraSequenceIntegrationTests.cs | 0 .../VirtualIoSequenceIntegrationTests.cs | 0 .../DeterministicConveyorWorkpieceTests.cs | 0 .../DeterministicInspectionHandoffTests.cs | 0 .../DeterministicInspectionSortRouterTests.cs | 0 .../DeterministicLoadLockTests.cs | 0 .../DeterministicMachineLayoutTests.cs | 0 .../DeterministicOhtHandoffTests.cs | 0 .../DeterministicPneumaticCylinderTests.cs | 0 .../DeterministicPrealignerTests.cs | 0 .../DeterministicWaferHandlerTests.cs | 0 .../DigitalSensorManualForceTests.cs | 0 ...ayoutRuntimeConfigurationValidatorTests.cs | 0 .../MachineLayoutRuntimeResetterTests.cs | 0 ...achineLayoutSignalBindingValidatorTests.cs | 0 .../DeterministicRecipeDryRunRunnerTests.cs | 0 .../DeterministicScenarioAssertionTests.cs | 0 .../DeterministicSignalHubTests.cs | 0 ...nisticSimulationBatchResultPackageTests.cs | 0 ...inisticSimulationCommandTraceStoreTests.cs | 0 ...eterministicSimulationCommandTraceTests.cs | 0 ...ministicSimulationRunResultPackageTests.cs | 0 .../FixedStepSimulationEngineTests.cs | 49 ++ .../{ => Runtime}/RuntimeGenerationTests.cs | 0 ...SimulationAutomaticRunCycleHandlerTests.cs | 0 .../SimulationAutomaticRunRuntimeTests.cs | 0 .../SimulationFaultRuntimeTests.cs | 0 .../SimulationPhysicalRuntimeTickTests.cs | 0 .../SimulationRunLoopTimingTests.cs | 0 ...ulationRuntimeConfigurationBuilderTests.cs | 0 .../SimulationRuntimeStateTests.cs | 0 .../SimulationSnapshotFactoryTests.cs | 0 .../DeterministicSequenceDebugStateTests.cs | 0 .../DeterministicSequenceExecutorTests.cs | 0 ...eterministicSequenceRuntimeContextTests.cs | 0 ...rministicSequenceStepPreviewRunnerTests.cs | 0 .../SequenceDefinitionEditorTests.cs | 0 .../SequenceStepNormalizationTests.cs | 0 .../SequenceStepTemplateCatalogTests.cs | 0 .../SimulationSequenceRuntimeTests.cs | 0 .../VirtualCameraFirstUseViewModelTests.cs | 3 +- .../MultiAxisCommissioningViewModelTests.cs | 95 +++ .../Hosting/MainWpfInteractionHostTests.cs | 52 ++ ...hineIntegrationPathReadinessPolicyTests.cs | 1 + ...tegrationResultObservationWorkflowTests.cs | 47 ++ .../MachineIntegrationSetupStoreTests.cs | 2 + ...hineIntegrationTcpControlViewModelTests.cs | 41 ++ ...achineIntegrationTcpOperationOwnerTests.cs | 1 + .../MachineIntegrationViewModelTests.cs | 158 ++--- .../OpenVisionLab.MachineStudio.Tests.csproj | 2 + .../ProjectLifecycleCoordinatorTests.cs | 5 +- .../Project/ProjectOpenWorkflowTests.cs | 7 +- .../Project/ProjectSaveWorkflowTests.cs | 2 + ...RecipeConnectionWorkbenchViewModelTests.cs | 145 +++-- .../SemiconductorRecipeCopyWorkflowTests.cs | 7 +- .../RuntimeObservabilityRetentionTests.cs | 0 .../FaultManagerViewModelTests.cs | 0 .../ScheduledFaultScenarioWorkflowTests.cs | 0 .../RuntimeAlarmCollectionViewModelTests.cs | 0 .../RuntimeDebuggerViewModelTests.cs | 0 .../RuntimeDebuggerWatchTargetCatalogTests.cs | 0 ...ntimeDefinitionApplicationWorkflowTests.cs | 0 .../RuntimeObservabilityJournalTests.cs | 0 .../RuntimeObservabilityPresenterTests.cs | 0 .../{ => Runtime}/RuntimeShutdownTests.cs | 0 ...ationCommandPresentationDispatcherTests.cs | 0 ...imulationRunControlAdmissionPolicyTests.cs | 0 .../SimulationRunControlWorkflowTests.cs | 0 .../SimulationRuntimeLoopTests.cs | 58 ++ .../SimulationRuntimeResourceOwnerTests.cs | 0 .../SimulationRuntimeShutdownWorkflowTests.cs | 40 +- ...imulationRuntimeSnapshotProjectionTests.cs | 0 .../SimulationSessionCloseWorkflowTests.cs | 0 .../SimulationSessionCoordinatorTests.cs | 0 ...mulationScenarioBatchArtifactStoreTests.cs | 0 ...SimulationScenarioBatchParticipantTests.cs | 0 ...imulationScenarioBatchPresentationTests.cs | 0 ...ationScenarioBatchRepetitionRunnerTests.cs | 0 .../SimulationScenarioBatchViewModelTests.cs | 248 ++++++++ ...lationScenarioExecutionCoordinatorTests.cs | 0 .../SimulationScenarioProfileTests.cs | 0 .../SimulationScenarioProjectMapperTests.cs | 0 .../SimulationScenarioWorkflowTests.cs | 0 .../SimulationScenarioBatchViewModelTests.cs | 82 --- .../CommandTraceWorkflowViewModelTests.cs | 0 .../SimulationCommandTraceViewModelTests.cs | 0 397 files changed, 4532 insertions(+), 2412 deletions(-) create mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationExchange.cs rename src/OpenVisionLab.Machine.Infrastructure/Integration/{ => Exchange}/MachineIntegrationHandoffPublisher.cs (100%) rename src/{OpenVisionLab.MachineStudio/ViewModel/Integration => OpenVisionLab.Machine.Infrastructure/Integration/Exchange}/MachineIntegrationHandoffRequestFactory.cs (58%) rename src/OpenVisionLab.Machine.Infrastructure/Integration/{ => Exchange}/MachineIntegrationLifecycle.cs (100%) rename src/{OpenVisionLab.MachineStudio/ViewModel/Integration => OpenVisionLab.Machine.Infrastructure/Integration/Exchange}/MachineIntegrationPathReadinessPolicy.cs (89%) rename src/{OpenVisionLab.MachineStudio/ViewModel/Integration => OpenVisionLab.Machine.Infrastructure/Integration/Exchange}/MachineIntegrationResultFileWatcher.cs (94%) rename src/{OpenVisionLab.MachineStudio/ViewModel/Integration => OpenVisionLab.Machine.Infrastructure/Integration/Exchange}/MachineIntegrationResultObservationWorkflow.cs (98%) rename src/{OpenVisionLab.MachineStudio/ViewModel/Integration => OpenVisionLab.Machine.Infrastructure/Integration/Exchange}/MachineIntegrationSetupStore.cs (92%) rename src/OpenVisionLab.Machine.Infrastructure/Integration/{ => Exchange}/MachineIntegrationTcpExchange.cs (100%) rename src/OpenVisionLab.Machine.Infrastructure/Integration/{ => Exchange}/MachineLocatorIntegrationRecipeContract.cs (100%) delete mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/MachineCoordinateProjectionContract.cs create mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionContract.cs create mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionMapper.cs create mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionPersistence.cs create mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionValidator.cs rename src/OpenVisionLab.Machine.Infrastructure/Integration/{ => Transactions}/MachineIntegrationTransactionFileSystem.cs (100%) rename src/OpenVisionLab.Machine.Infrastructure/Integration/{MachineIntegrationTransactionMaintenance.cs => Transactions/MachineIntegrationTransactionInspector.cs} (63%) rename src/OpenVisionLab.Machine.Infrastructure/Integration/{MachineIntegrationExchange.cs => Transactions/MachineIntegrationTransactionPublisher.cs} (70%) create mode 100644 src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionRetention.cs create mode 100644 src/OpenVisionLab.Machine.Persistence/OpenVisionLab.Machine.Persistence.csproj create mode 100644 src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileCatalog.cs create mode 100644 src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileStore.cs rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/ConfigureAxesCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/HomeAxisCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/JogAxisCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/MoveAbsoluteCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/MoveRelativeCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/MoveVelocityCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/StopAxesCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Axis}/StopAxisCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Camera}/TriggerVirtualCameraCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Equipment}/SetConveyorCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Equipment}/SetCylinderCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Equipment}/SetDigitalSensorForceCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Equipment}/SetVirtualInputCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Equipment}/SetVirtualInputForceCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Faults}/ClearSimulationFaultCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Faults}/InjectSimulationFaultCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/ConfigureRuntimeCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/MoveAxesAbsoluteCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/PauseCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/PlayCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/ResetCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/SimulationCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/SimulationCommandResult.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/StartAutomaticRunCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/StartConditionScenarioCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/StartManualControlCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/StepCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => RunControl}/StopConditionScenarioCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Sequence}/AbortSequenceCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Sequence}/RetrySequenceCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Sequence}/SetSequenceBreakpointCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Sequence}/StartSequenceCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Commands/{ => Sequence}/StepSequenceCommand.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => AutomaticRun}/AutomaticRunConfiguration.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => AutomaticRun}/SimulationAutomaticRunCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => AutomaticRun}/SimulationAutomaticRunCycleHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => AutomaticRun}/SimulationAutomaticRunRuntime.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ConditionScenario}/SimulationConditionScenarioCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ConditionScenario}/SimulationConditionScenarioProgressHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ConditionScenario}/SimulationConditionScenarioRuntime.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ConditionScenario}/SimulationConditionScenarioStopHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Faults}/SimulationConditionScheduledFaultInjectionHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Faults}/SimulationConditionScheduledFaultRecoveryHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Faults}/SimulationEngineFaultPoint.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Faults}/SimulationFaultCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Faults}/SimulationFaultRuntime.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ManualControl}/SimulationManualAxisCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ManualControl}/SimulationManualCameraCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ManualControl}/SimulationManualControlCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ManualControl}/SimulationManualEquipmentCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ManualControl}/SimulationManualInputCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => ManualControl}/SimulationPhysicalRuntimeTick.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/DeterministicSimulationCommandTraceStore.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/FixedStepSimulationEngine.cs (99%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/ISimulationEngine.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationClock.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationControlOwner.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationEngineLifecycle.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationEngineTermination.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationRunControlCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationRunLoopTiming.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationRunMode.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationRuntimeConfiguration.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationRuntimeConfigurationBuilder.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationRuntimeState.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Runtime}/SimulationSettings.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Sequence}/DeterministicSequenceRuntimeContext.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Sequence}/SimulationSequenceCommandHandler.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Engine/{ => Sequence}/SimulationSequenceRuntime.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Assertions}/DeterministicScenarioAssertionEvaluator.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Assertions}/DeterministicScenarioAssertions.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Batch}/DeterministicSimulationBatchResultComparer.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Batch}/DeterministicSimulationBatchResultPackage.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Batch}/DeterministicSimulationBatchRunner.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => CommandTrace}/DeterministicSimulationCommandTrace.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => CommandTrace}/DeterministicSimulationCommandTraceCommandCodec.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => CommandTrace}/DeterministicSimulationCommandTraceReplayRunner.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Commissioning}/DeterministicUnifiedCommissioningEvidencePackage.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Condition}/DeterministicConditionScenarioProfile.cs (97%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Condition}/DeterministicConditionScenarioReplayResult.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Condition}/DeterministicConditionScenarioRunner.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Condition}/DeterministicConditionScenarioRuntime.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Evidence}/AtomicEvidenceFile.cs (54%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Evidence}/DeterministicSimulationEvidenceExchangePackage.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Evidence}/DeterministicSimulationRunEvidenceHasher.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Evidence}/DeterministicSimulationRunResultComparer.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Evidence}/DeterministicSimulationRunResultPackage.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Evidence}/SimulationEvidenceRetentionPolicy.cs (100%) rename src/OpenVisionLab.Machine.Simulation/Scenarios/{ => Vision}/DeterministicVisionExecutionEvidencePackage.cs (81%) create mode 100644 src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionRecorder.cs rename src/OpenVisionLab.MachineStudio/Smoke/{ => Camera}/SmokeCameraCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Camera}/SmokeCameraFirstUseVerifier.cs (99%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeAnalogIoAuthoringVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeAxisCommissioningStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeAxisCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeAxisTuningStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeConveyorCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeCylinderCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeDigitalIoCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeLoadLockSetupVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeMultiAxisCommissioningStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeMultiAxisCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Commissioning}/SmokeSensorCommissioningVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeConnectionWorkbenchWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeFaultScenarioHost.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeSmokeArgumentParser.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeSmokeFailurePolicy.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeSmokeHost.cs (99%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeSmokeOptions.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/DirectExeSmokeProjectLoader.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/MachineIntegrationExeSmoke.cs (96%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/MachineIntegrationExeSmokeCameraScenario.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Host}/SmokePerformanceVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Integration}/SmokeConnectionWorkbenchVerifier.cs (99%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Integration}/SmokeIntegrationResultVerifier.cs (98%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeCanvasNavigationVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeDirectSceneAuthoringVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeDirectTransformVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeEditMenuStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeEvidenceDrawerStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeGlobalCommandStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeLayerOrderVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeLayoutAlignmentVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeLayoutHistoryVerifier.cs (99%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeLayoutPropertyStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeLibraryDropVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Layout}/SmokeMultiSelectionTransformVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Project}/SmokeProjectRoundTripVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Project}/SmokeProjectSafetyVerifier.cs (76%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Project}/SmokeProjectTreeQuery.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeBatchPersistenceVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokePickAndPlaceStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockApplicationVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockEditVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockPreparation.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockProposalVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockSequenceStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockStepStatusVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeProcessBlockTimeoutVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeRecipeCheckpointStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeRecipeConnectionStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeRecipeDryRunStateVerifier.cs (99%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeRecipeGalleryVerifier.cs (99%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeSemanticSetupVerifier.cs (64%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Recipes}/SmokeStationSkeletonVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Runtime}/SmokeFaultManagerVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Runtime}/SmokeRuntimeDebuggerVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Runtime}/SmokeRuntimeEvidenceVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Runtime}/SmokeScenarioBatchVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Runtime}/SmokeSequenceStateVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Runtime}/SmokeTestScenarioRuntimeVerifier.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeButtonPointerState.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeNativeInput.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeRoundTripScenario.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeUiInteraction.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeVisualTreeQuery.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeWindowCapture.cs (100%) rename src/OpenVisionLab.MachineStudio/Smoke/{ => Support}/SmokeWorkflowReport.cs (100%) create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupViewModel.cs rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Authoring}/ProcessPlanReviewViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Authoring}/RecipeAuthoringWorkspace.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Authoring}/RecipeCheckpointTemplateViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Authoring}/RecipeProcessBlockViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Connections}/RecipeConnectionProjectApplier.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Connections}/RecipeConnectionRowCatalog.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Connections}/RecipeConnectionSetupWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Connections}/RecipeConnectionSimulationWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Connections}/RecipeConnectionWorkbenchViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => DryRun}/RecipeDryRunPlaybackViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => DryRun}/RecipeDryRunViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => DryRun}/RecipeSequenceStepPreviewViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/RecipePackCompatibilityComparisonItemViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/SemiconductorRecipeCopyWorkflow.cs (91%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/SemiconductorRecipeGalleryCatalog.cs (84%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/SemiconductorRecipeGalleryItemViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/SemiconductorRecipeGalleryValidationSession.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/SemiconductorRecipeGalleryValidationWorkflow.cs (95%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Gallery}/SemiconductorRecipeGalleryViewModel.cs (100%) create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionHandoffSetupViewModel.cs create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionSortRouterSetupViewModel.cs create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/OhtHandoffSetupViewModel.cs create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/PrealignerSetupViewModel.cs create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/SemanticEquipmentSetupViewModel.cs create mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/WaferHandlerSetupViewModel.cs delete mode 100644 src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemanticEquipmentSetupViewModel.cs rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Station}/LoadLockSetupViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Recipes/{ => Station}/StationSkeletonSetupViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Faults}/FaultManagerViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Faults}/ScheduledFaultScenarioWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/BoundedShutdownCoordinator.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeAlarmCollectionViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeDebuggerViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeDebuggerWatchTargetCatalog.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeDefinitionApplicationWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeObservabilityJournal.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeObservabilityPresenter.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/RuntimeTimelineViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRunControlAdmissionPolicy.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRunControlWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRuntimeLoop.cs (99%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRuntimeProjectionCoordinator.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRuntimeResourceOwner.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRuntimeShutdownWorkflow.cs (90%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Runtime}/SimulationRuntimeSnapshotProjection.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioBatchArtifactStore.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioBatchParticipant.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioBatchPresentation.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioBatchRepetitionRunner.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioBatchViewModel.cs (91%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioExecutionCoordinator.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Scenarios}/SimulationScenarioWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Session}/SimulationSessionCloseWorkflow.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Session}/SimulationSessionCoordinator.cs (99%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Session}/SimulationWorkspaceViewModel.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Trace}/SimulationCommandPresentationDispatcher.cs (100%) rename src/OpenVisionLab.MachineStudio/ViewModel/Simulation/{ => Trace}/SimulationCommandTraceViewModel.cs (100%) rename tests/{OpenVisionLab.MachineStudio.Tests/Integration => OpenVisionLab.Machine.Infrastructure.Tests}/MachineIntegrationHandoffRequestFactoryTests.cs (73%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Axis}/ServoAxisComponentTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Axis}/TrapezoidalMotionProfileTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Camera}/DeterministicVirtualCameraTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Camera}/VirtualCameraInspectionTemplateTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationAutomaticRunCommandHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationConditionScenarioCommandHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationConditionScenarioProgressHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationConditionScenarioStopHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationConditionScheduledFaultInjectionHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationConditionScheduledFaultRecoveryHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationFaultCommandHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationManualControlCommandHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationRunControlCommandHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commands}/SimulationSequenceCommandHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Commissioning}/MultiAxisCommissioningRecipeTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/FixedStepDelayConverterTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/MachineLayoutWaferHandlerRuntimeCompilerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/MachineProjectRuntimeCompilerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/MachineProjectRuntimeComponentCompilerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/PrealignerCompilerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/RepresentativeRecipeCheckpointTemplateTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/SemiconductorProcessBlockComposerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/SemiconductorStationSkeletonTemplateTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Compilation}/SequenceCompilerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/AtomicEvidenceFileTests.cs (83%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/ConcreteFaultRepeatComparisonTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/DeterministicSimulationEvidenceExchangePackageTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/DeterministicUnifiedCommissioningEvidencePackageTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/DeterministicVisionExecutionEvidencePackageTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/PickAndPlaceFaultRecoveryEvidenceTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/ScaleEvidenceTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/SimulationEvidenceRetentionPolicyTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/SimulationOutputStoreTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Evidence}/SustainedScaleEvidenceTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/AutomaticTransferCellScheduledFaultRecoveryTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/DeterministicConditionScenarioTests.cs (99%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/DeterministicFaultInjectionTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/DeterministicFaultScenarioHeadlessRunnerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/DeterministicFaultScenarioProfileTests.cs (72%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/DeterministicFaultScenarioReplayTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/SimulationConditionScenarioRuntimeTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Faults}/SimulationFaultTargetCatalogTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/AutomaticRunIntegrationTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/AutomaticTransferCellEndToEndTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/MachineLayoutAutomaticRunIntegrationTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/PickAndPlaceSampleEndToEndTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/SemiconductorRecipeEndToEndTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/SubsequenceCompositionTests.cs (88%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/VirtualCameraSequenceIntegrationTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Integration}/VirtualIoSequenceIntegrationTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicConveyorWorkpieceTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicInspectionHandoffTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicInspectionSortRouterTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicLoadLockTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicMachineLayoutTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicOhtHandoffTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicPneumaticCylinderTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicPrealignerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DeterministicWaferHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/DigitalSensorManualForceTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/MachineLayoutRuntimeConfigurationValidatorTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/MachineLayoutRuntimeResetterTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Layout}/MachineLayoutSignalBindingValidatorTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicRecipeDryRunRunnerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicScenarioAssertionTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicSignalHubTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicSimulationBatchResultPackageTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicSimulationCommandTraceStoreTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicSimulationCommandTraceTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/DeterministicSimulationRunResultPackageTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/FixedStepSimulationEngineTests.cs (95%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/RuntimeGenerationTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationAutomaticRunCycleHandlerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationAutomaticRunRuntimeTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationFaultRuntimeTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationPhysicalRuntimeTickTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationRunLoopTimingTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationRuntimeConfigurationBuilderTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationRuntimeStateTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Runtime}/SimulationSnapshotFactoryTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/DeterministicSequenceDebugStateTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/DeterministicSequenceExecutorTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/DeterministicSequenceRuntimeContextTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/DeterministicSequenceStepPreviewRunnerTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/SequenceDefinitionEditorTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/SequenceStepNormalizationTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/SequenceStepTemplateCatalogTests.cs (100%) rename tests/OpenVisionLab.Machine.Simulation.Tests/{ => Sequence}/SimulationSequenceRuntimeTests.cs (100%) create mode 100644 tests/OpenVisionLab.MachineStudio.Tests/Hosting/MainWpfInteractionHostTests.cs rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Evidence}/RuntimeObservabilityRetentionTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Faults}/FaultManagerViewModelTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Faults}/ScheduledFaultScenarioWorkflowTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeAlarmCollectionViewModelTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeDebuggerViewModelTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeDebuggerWatchTargetCatalogTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeDefinitionApplicationWorkflowTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeObservabilityJournalTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeObservabilityPresenterTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/RuntimeShutdownTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationCommandPresentationDispatcherTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationRunControlAdmissionPolicyTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationRunControlWorkflowTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationRuntimeLoopTests.cs (72%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationRuntimeResourceOwnerTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationRuntimeShutdownWorkflowTests.cs (85%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationRuntimeSnapshotProjectionTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationSessionCloseWorkflowTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Runtime}/SimulationSessionCoordinatorTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioBatchArtifactStoreTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioBatchParticipantTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioBatchPresentationTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioBatchRepetitionRunnerTests.cs (100%) create mode 100644 tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchViewModelTests.cs rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioExecutionCoordinatorTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioProfileTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioProjectMapperTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Scenarios}/SimulationScenarioWorkflowTests.cs (100%) delete mode 100644 tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchViewModelTests.cs rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Trace}/CommandTraceWorkflowViewModelTests.cs (100%) rename tests/OpenVisionLab.MachineStudio.Tests/Simulation/{ => Trace}/SimulationCommandTraceViewModelTests.cs (100%) diff --git a/Directory.Build.props b/Directory.Build.props index 167c8f4..e308ba4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 0.2.0-dev.19 + 0.2.0-dev.20 false 12.0 enable diff --git a/OpenVisionLab.MachineStudio.sln b/OpenVisionLab.MachineStudio.sln index 5bd00f8..723f953 100644 --- a/OpenVisionLab.MachineStudio.sln +++ b/OpenVisionLab.MachineStudio.sln @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVisionLab.MachineStudio EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ABCDEF12-3456-7890-ABCD-EF0123456789}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVisionLab.Machine.Persistence", "src\OpenVisionLab.Machine.Persistence\OpenVisionLab.Machine.Persistence.csproj", "{F05C6B7F-6D1C-419B-85D4-176F3BC6243D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -108,5 +110,12 @@ Global {A4B5C6D7-E8F9-0123-4567-8901234DEF02}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4B5C6D7-E8F9-0123-4567-8901234DEF02}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4B5C6D7-E8F9-0123-4567-8901234DEF02}.Release|Any CPU.Build.0 = Release|Any CPU + {F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F05C6B7F-6D1C-419B-85D4-176F3BC6243D} = {12345678-1234-1234-1234-123456789ABC} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 781406e..72f7f5e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ single-step, reset, dry-run, and layout playback workflows. ## Version -Current version: `v0.2.0-dev.19` +Current version: `v0.2.0-dev.20` This project is maintained using explicit version numbers. The current branch is a Release 2 development candidate; it is not a release tag or downloadable @@ -21,6 +21,11 @@ package. ### Recent version history +#### `v0.2.0-dev.20` (2026-09-10) + +- Organized the machine, simulation, sequence, vision, fault, and result code into responsibility folders while preserving namespaces and runtime contracts. +- Added headless shutdown termination coverage and kept the Release build plus 1,102 automated tests green. + #### `v0.2.0-dev.19` (2026-09-09) - Preserved cancellation requests until the next bounded observation so a just-completed batch or commissioning validation is reported as `Cancelled`, not `Idle`. @@ -66,12 +71,6 @@ package. - Extracted wafer-handler runtime compilation into the WPF-neutral `MachineLayoutWaferHandlerRuntimeCompiler`. - Preserved layout-device orchestration and verified focused, full Simulation, MachineStudio Sequence, and Release build gates. -#### `v0.2.0-dev.10` (2026-09-09) - -- Extracted cross-sequence identity and cycle validation into the WPF-neutral `SequenceCompositionValidator`. -- Preserved the `SequenceCompiler` public compatibility entry point and verified Sequence, MachineStudio, full Simulation, and Release build gates. - - ## Download Download the latest Windows x64 ZIP from diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5936d51..5ec7e03 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -71,21 +71,44 @@ Camera acquisition and Vision evidence remain supported secondary capabilities. They join a machine cycle when an inspection task requires them; they do not own the core equipment-simulation workflow. -For current source navigation, read [DEVELOPER_ONBOARDING.md](DEVELOPER_ONBOARDING.md) -and [MVVM_ARCHITECTURE.md](MVVM_ARCHITECTURE.md). MachineStudio feature owners -live in shallow `ViewModel` domain folders; `MainViewModel.cs` remains the root -composition entrypoint. Folder movement preserves existing namespaces and does -not itself change the module dependency graph below. +## Source navigation + +The shortest code-reading route follows the runtime rather than the UI tree: + +```text +MachineStudio/MainViewModel + -> ViewModel/Simulation/Session/SimulationSessionCoordinator + -> Simulation/Engine/Runtime/FixedStepSimulationEngine + -> Simulation/Commands/Axis and Commands/Equipment (axis/I/O commands) + -> Sequence/Runtime and Simulation/Engine/Sequence + -> Vision contracts and Simulation/Scenarios/Vision + -> Simulation/Engine/Faults + -> Simulation/Scenarios/Evidence (result/evidence packages) +``` + +The physical folders make the ownership visible without changing namespaces or +public bindings. `ViewModel/Recipes` is grouped by authoring, dry-run, gallery, +semantic equipment, and station setup; `ViewModel/Simulation` is grouped by +faults, runtime, scenarios, session, and trace. Simulation commands are grouped +by axis, camera, equipment, faults, run control, and sequence. The corresponding +simulation tests use the same responsibility names, so a caller and its focused +test can be found by the same search term. + +`MainViewModel.cs` remains the WPF composition entrypoint. Folder movement +preserves existing namespaces, XAML bindings, and the module dependency graph. ## Layered architecture ```text MachineStudio (WPF composition and presentation) - -> Core, IO, Sequence, Simulation, Vision, Infrastructure + -> Core, IO, Persistence, Sequence, Simulation, Vision, Infrastructure Infrastructure (project assets and external adapters) -> Vision +Persistence (project JSON file adapter and catalog) + -> Core + Simulation (project compiler, fixed-step composition, runtime truth, deterministic execution evidence) -> Core, IO, Sequence diff --git a/docs/NET_VERSION_POLICY.md b/docs/NET_VERSION_POLICY.md index 1c7e5d4..cb96445 100644 --- a/docs/NET_VERSION_POLICY.md +++ b/docs/NET_VERSION_POLICY.md @@ -6,7 +6,7 @@ - Language version: C# 12 - Nullable: enable - ImplicitUsings: enable -- Product version: `0.2.0-dev.19` +- Product version: `0.2.0-dev.20` The public repository is currently on the Release 2 development line. This is not a release tag or a downloadable release; the release-candidate gates below diff --git a/src/OpenVisionLab.Machine.Core/Projects/ProjectDocumentStore.cs b/src/OpenVisionLab.Machine.Core/Projects/ProjectDocumentStore.cs index 4c2a295..f8543bd 100644 --- a/src/OpenVisionLab.Machine.Core/Projects/ProjectDocumentStore.cs +++ b/src/OpenVisionLab.Machine.Core/Projects/ProjectDocumentStore.cs @@ -1,4 +1,3 @@ -using System.Runtime.ExceptionServices; using System.Text.Json; using System.Text.Json.Nodes; @@ -75,73 +74,11 @@ public MachineProjectDocument Load(string json) return doc; } - public async Task SaveAsync(MachineProjectDocument document, string path, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(document); - ArgumentException.ThrowIfNullOrWhiteSpace(path); - var fullPath = Path.GetFullPath(path); - var directory = Path.GetDirectoryName(fullPath) - ?? throw new ArgumentException("The project path must include a directory.", nameof(path)); - var temporaryPath = Path.Combine( - directory, - $".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp"); - var modifiedAt = DateTimeOffset.UtcNow; - var json = SerializeForSave(document, modifiedAt); - - try - { - await File.WriteAllTextAsync(temporaryPath, json, cancellationToken).ConfigureAwait(false); - if (File.Exists(fullPath)) - { - File.Replace(temporaryPath, fullPath, fullPath + ".bak", ignoreMetadataErrors: true); - } - else - { - File.Move(temporaryPath, fullPath); - } - - ApplySaveMetadata(document, modifiedAt); - } - finally - { - if (File.Exists(temporaryPath)) - { - File.Delete(temporaryPath); - } - } - } - - public async Task LoadAsync(string path, CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(path); - var fullPath = Path.GetFullPath(path); - try - { - return await LoadFileAsync(fullPath, cancellationToken).ConfigureAwait(false); - } - catch (Exception primaryException) when (ShouldTryBackup(primaryException)) - { - try - { - return await LoadFileAsync(fullPath + ".bak", cancellationToken).ConfigureAwait(false); - } - catch (Exception backupException) when (IsExpectedLoadFailure(backupException)) - { - if (backupException is ProjectDocumentLoadException - { - ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema - }) - { - ExceptionDispatchInfo.Capture(backupException).Throw(); - } - - ExceptionDispatchInfo.Capture(primaryException).Throw(); - throw; - } - } - } - - private string SerializeForSave(MachineProjectDocument document, DateTimeOffset modifiedAt) + /// + /// Creates the canonical on-disk JSON payload without mutating the document. + /// The file adapter applies the returned timestamp only after its atomic commit succeeds. + /// + public string SerializeForSave(MachineProjectDocument document, DateTimeOffset modifiedAt) { var root = JsonNode.Parse(Serialize(document))?.AsObject() ?? throw new InvalidOperationException("Failed to serialize project document."); @@ -155,31 +92,4 @@ private static void ApplySaveMetadata(MachineProjectDocument document, DateTimeO document.Schema = MachineProjectDocument.CurrentSchema; document.ModifiedAt = modifiedAt; } - - private async Task LoadFileAsync( - string path, - CancellationToken cancellationToken) - { - var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); - return Load(json); - } - - private static bool ShouldTryBackup(Exception exception) => exception switch - { - ProjectDocumentLoadException - { - ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema - } => false, - _ => IsExpectedLoadFailure(exception) - }; - - private static bool IsExpectedLoadFailure(Exception exception) => exception switch - { - ProjectDocumentLoadException => true, - JsonException => true, - IOException => true, - UnauthorizedAccessException => true, - ArgumentOutOfRangeException => true, - _ => false - }; } diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationExchange.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationExchange.cs new file mode 100644 index 0000000..d1fcc5c --- /dev/null +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationExchange.cs @@ -0,0 +1,92 @@ +using OpenVisionLab.Integration.Contracts; + +namespace OpenVisionLab.Machine.Infrastructure.Integration; + +public sealed record MachineIntegrationTransactionSummary( + IntegrationHandoffV2 Handoff, + bool HasAcknowledgement, + bool HasResult); + +/// +/// Public compatibility facade for the Machine Studio side of the v2 file exchange. +/// Each operation delegates to the concrete owner for publication, inspection, or +/// retention. The facade contains no transaction policy or file-system workflow. +/// +public static class MachineIntegrationExchange +{ + public const long DefaultMinimumFreeSpaceBytes = 1_048_576; + + public static IntegrationHandoffV2 PublishHandoff( + string exchangeRoot, + IntegrationHandoffV2 handoff, + IReadOnlyDictionary artifactSourcePaths) => + MachineIntegrationTransactionPublisher.PublishHandoff( + exchangeRoot, + handoff, + artifactSourcePaths); + + public static Task PublishHandoffAsync( + string exchangeRoot, + IntegrationHandoffV2 handoff, + IReadOnlyDictionary artifactSourcePaths, + IProgress? progress = null, + long minimumFreeSpaceBytes = DefaultMinimumFreeSpaceBytes, + CancellationToken cancellationToken = default) => + MachineIntegrationTransactionPublisher.PublishHandoffAsync( + exchangeRoot, + handoff, + artifactSourcePaths, + progress, + minimumFreeSpaceBytes, + cancellationToken); + + public static IReadOnlyList DiscoverTransactions( + string exchangeRoot) => + MachineIntegrationTransactionInspector.DiscoverTransactions(exchangeRoot); + + public static IntegrationHandoffV2 ReadHandoff( + string exchangeRoot, + Guid transactionId) => + MachineIntegrationTransactionInspector.ReadHandoff(exchangeRoot, transactionId); + + public static IntegrationHandoffV2 ReadHandoffEnvelope( + string exchangeRoot, + Guid transactionId) => + MachineIntegrationTransactionInspector.ReadHandoffEnvelope(exchangeRoot, transactionId); + + public static IntegrationAcknowledgementV2 ReadAcknowledgement( + string exchangeRoot, + Guid transactionId) => + MachineIntegrationTransactionInspector.ReadAcknowledgement(exchangeRoot, transactionId); + + public static IntegrationResultV2 ReadResult( + string exchangeRoot, + Guid transactionId) => + MachineIntegrationTransactionInspector.ReadResult(exchangeRoot, transactionId); + + public static IReadOnlyList DiagnoseTransactions( + string exchangeRoot) => + MachineIntegrationTransactionInspector.DiagnoseTransactions(exchangeRoot); + + public static MachineIntegrationCleanupReport CleanupStaging( + string exchangeRoot, + TimeSpan staleAfter, + DateTimeOffset? nowUtc = null, + CancellationToken cancellationToken = default) => + MachineIntegrationTransactionRetention.CleanupStaging( + exchangeRoot, + staleAfter, + nowUtc, + cancellationToken); + + public static int PurgeQuarantine( + string exchangeRoot, + TimeSpan retention, + DateTimeOffset? nowUtc = null, + CancellationToken cancellationToken = default) => + MachineIntegrationTransactionRetention.PurgeQuarantine( + exchangeRoot, + retention, + nowUtc, + cancellationToken); +} diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationHandoffPublisher.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationHandoffPublisher.cs similarity index 100% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationHandoffPublisher.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationHandoffPublisher.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationHandoffRequestFactory.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationHandoffRequestFactory.cs similarity index 58% rename from src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationHandoffRequestFactory.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationHandoffRequestFactory.cs index 95c588c..cd23bcb 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationHandoffRequestFactory.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationHandoffRequestFactory.cs @@ -1,13 +1,65 @@ using System.IO; using System.Security.Cryptography; using OpenVisionLab.Integration.Contracts; -using OpenVisionLab.Machine.Infrastructure.Integration; using OpenVisionLab.Machine.Infrastructure.Vision; -using OpenVisionLab.Machine.Simulation.Camera; -namespace OpenVisionLab.MachineStudio.ViewModel; +namespace OpenVisionLab.Machine.Infrastructure.Integration; -internal sealed record MachineIntegrationHandoffRequestInput( +/// +/// Immutable frame identity passed from a camera application into the +/// integration boundary. It contains no Simulation or WPF type. +/// +public sealed record MachineIntegrationFrameEvidence +{ + public MachineIntegrationFrameEvidence( + string frameId, + string sourceRelativePath, + string contentSha256, + long contentLength) + { + ArgumentException.ThrowIfNullOrWhiteSpace(frameId); + ArgumentException.ThrowIfNullOrWhiteSpace(sourceRelativePath); + if (Path.IsPathRooted(sourceRelativePath) + || sourceRelativePath.Split(['/', '\\']).Any(segment => segment == "..")) + { + throw new ArgumentException( + "Frame source must be a project-relative path without parent traversal.", + nameof(sourceRelativePath)); + } + if (string.IsNullOrWhiteSpace(contentSha256) + || contentSha256.Length != 64 + || contentSha256.Any(character => !Uri.IsHexDigit(character))) + { + throw new ArgumentException( + "Content SHA-256 must contain exactly 64 hexadecimal characters.", + nameof(contentSha256)); + } + if (contentLength <= 0) + { + throw new ArgumentOutOfRangeException(nameof(contentLength)); + } + + FrameId = frameId; + SourceRelativePath = sourceRelativePath; + ContentSha256 = contentSha256.ToUpperInvariant(); + ContentLength = contentLength; + } + + public string FrameId { get; } + + public string SourceRelativePath { get; } + + public string ContentSha256 { get; } + + public long ContentLength { get; } +} + +/// +/// Input snapshot for creating a two-dimensional inspection handoff. The +/// caller owns Simulation-context eligibility; this owner verifies the source +/// file and composes the external request. +/// +public sealed record MachineIntegrationHandoffRequestInput( string ProjectId, string ProjectSchema, string SequenceId, @@ -19,11 +71,16 @@ internal sealed record MachineIntegrationHandoffRequestInput( int SourceWidth, int SourceHeight, string InspectionRecipePath, - VirtualCameraFrameEvidence FrameEvidence, + MachineIntegrationFrameEvidence FrameEvidence, IntegrationApplicationIdentity Producer, IntegrationApplicationIdentity Consumer); -internal sealed class MachineIntegrationHandoffRequestFactory +/// +/// Verifies a project-owned camera source against immutable frame evidence and +/// creates the external two-dimensional inspection request. It is independent +/// of the WPF host and Simulation project. +/// +public sealed class MachineIntegrationHandoffRequestFactory { public MachineInspectionHandoffRequest? Create( MachineIntegrationHandoffRequestInput input) diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationLifecycle.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationLifecycle.cs similarity index 100% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationLifecycle.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationLifecycle.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationPathReadinessPolicy.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationPathReadinessPolicy.cs similarity index 89% rename from src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationPathReadinessPolicy.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationPathReadinessPolicy.cs index 36f54fe..532dca0 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationPathReadinessPolicy.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationPathReadinessPolicy.cs @@ -1,13 +1,13 @@ using System.IO; -namespace OpenVisionLab.MachineStudio.ViewModel; +namespace OpenVisionLab.Machine.Infrastructure.Integration; /// /// Owns the shared path normalization and current file-system readiness checks /// used by Machine Studio integration entry points. It does not own setup /// persistence, handoff eligibility, transaction data, or watcher lifetime. /// -internal sealed class MachineIntegrationPathReadinessPolicy +public sealed class MachineIntegrationPathReadinessPolicy { public MachineIntegrationPathReadinessSnapshot Evaluate( string? exchangeRoot, @@ -36,7 +36,7 @@ public string NormalizeFullPath(string? path) => : Path.GetFullPath(Normalize(path)); } -internal sealed record MachineIntegrationPathReadinessSnapshot( +public sealed record MachineIntegrationPathReadinessSnapshot( string ExchangeRoot, string InspectionRecipePath, bool IsExchangeRootAvailable, diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationResultFileWatcher.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationResultFileWatcher.cs similarity index 94% rename from src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationResultFileWatcher.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationResultFileWatcher.cs index 64df3a0..b0d9ec6 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationResultFileWatcher.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationResultFileWatcher.cs @@ -2,7 +2,7 @@ using System.Text.Json; using OpenVisionLab.Integration.Contracts; -namespace OpenVisionLab.MachineStudio.ViewModel; +namespace OpenVisionLab.Machine.Infrastructure.Integration; /// /// Owns the file-system watcher and debounced refresh lifetime for Machine @@ -141,7 +141,15 @@ or JsonException { if (!IsDisposed) { - _handleAutomaticRefreshException(exception); + await _invokeOnUiThreadAsync(() => + { + if (!IsDisposed) + { + _handleAutomaticRefreshException(exception); + } + + return Task.CompletedTask; + }).ConfigureAwait(false); } } finally diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationResultObservationWorkflow.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationResultObservationWorkflow.cs similarity index 98% rename from src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationResultObservationWorkflow.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationResultObservationWorkflow.cs index 08ee1b0..43b597b 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationResultObservationWorkflow.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationResultObservationWorkflow.cs @@ -1,16 +1,15 @@ using System.IO; using System.Text.Json; using OpenVisionLab.Integration.Contracts; -using OpenVisionLab.Machine.Infrastructure.Integration; -namespace OpenVisionLab.MachineStudio.ViewModel; +namespace OpenVisionLab.Machine.Infrastructure.Integration; /// /// Owns file-backed integration transaction observation. It does not publish /// a handoff, acknowledge a transaction, or start an inspection; the concrete /// result-file watcher owns its event and cancellation lifetime. /// -internal sealed class MachineIntegrationResultObservationWorkflow : IDisposable +public sealed class MachineIntegrationResultObservationWorkflow : IDisposable { private readonly Func _exchangeRootProvider; private readonly Func _projectIdProvider; diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupStore.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationSetupStore.cs similarity index 92% rename from src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupStore.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationSetupStore.cs index a50561b..c3c4ac8 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupStore.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationSetupStore.cs @@ -2,21 +2,21 @@ using System.Net; using System.Text.Json; -namespace OpenVisionLab.MachineStudio.ViewModel; +namespace OpenVisionLab.Machine.Infrastructure.Integration; -internal enum MachineIntegrationSetupLoadWarning +public enum MachineIntegrationSetupLoadWarning { None, MissingOrInvalid, ReadFailed } -internal sealed record MachineIntegrationSetupLoadResult( +public sealed record MachineIntegrationSetupLoadResult( MachineIntegrationSetup Settings, MachineIntegrationSetupLoadWarning Warning, string? ErrorMessage); -internal sealed record MachineIntegrationSetup +public sealed record MachineIntegrationSetup { public string ExchangeRoot { get; init; } = string.Empty; public string InspectionRecipePath { get; init; } = string.Empty; @@ -28,7 +28,7 @@ internal sealed record MachineIntegrationSetup public int TcpPeerPort { get; init; } = 45102; } -internal sealed record MachineIntegrationTcpSettings( +public sealed record MachineIntegrationTcpSettings( string ExchangeRoot, IPAddress ListenAddress, int ListenPort, @@ -39,7 +39,7 @@ internal sealed record MachineIntegrationTcpSettings( /// Owns the file-backed Machine Studio integration setup format and its /// atomic persistence. It has no WPF or ViewModel dependency. /// -internal sealed class MachineIntegrationSetupStore +public sealed class MachineIntegrationSetupStore { private readonly string _path; @@ -91,6 +91,11 @@ public MachineIntegrationSetupLoadResult Load() public void Save(MachineIntegrationSetup settings) { ArgumentNullException.ThrowIfNull(settings); + if (!string.IsNullOrWhiteSpace(settings.ExchangeRoot)) + { + Directory.CreateDirectory(Path.GetFullPath(settings.ExchangeRoot)); + } + var fullPath = Path.GetFullPath(_path); var directory = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("The settings path must include a directory.", nameof(_path)); diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTcpExchange.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationTcpExchange.cs similarity index 100% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTcpExchange.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineIntegrationTcpExchange.cs diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineLocatorIntegrationRecipeContract.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineLocatorIntegrationRecipeContract.cs similarity index 100% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineLocatorIntegrationRecipeContract.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Exchange/MachineLocatorIntegrationRecipeContract.cs diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineCoordinateProjectionContract.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineCoordinateProjectionContract.cs deleted file mode 100644 index 14ebc94..0000000 --- a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineCoordinateProjectionContract.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System.Text.Json; - -namespace OpenVisionLab.Machine.Infrastructure.Integration; - -/// -/// Stable sidecar contract for the first cross-modal projection slice. The -/// profile describes software coordinates only: image pixels and C3D -/// grid-index coordinates both use a top-left origin. -/// -public static class MachineCoordinateProjectionContract -{ - public const string SchemaVersion = "1.0"; - public const string ProfileArtifactRole = "coordinate-projection-profile"; - public const string ProfileArtifactId = "coordinate-projection-profile"; - public const string ResultEvidenceRole = "coordinate-projection-result"; - public const string ResultEvidenceArtifactId = "coordinate-projection-result"; - public const string MappingKind = "normalized-linear"; - public const string ImageUnit = "px"; - public const string ImageOrigin = "top-left"; - public const string GridUnit = "raw-height"; - public const string GridFrameId = "frame.c3d-grid-index"; - public const string GridOrigin = "top-left"; - - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - - public static MachineCoordinateProjectionProfile CreateDefault( - string projectionId, - int imageWidth, - int imageHeight, - string? twoDTransactionId = null) => - new( - SchemaVersion, - RequireText(projectionId, nameof(projectionId)), - twoDTransactionId, - new MachineCoordinateProjectionImage( - imageWidth, - imageHeight, - ImageUnit, - ImageOrigin), - new MachineCoordinateProjectionGrid( - GridUnit, - GridFrameId, - GridOrigin), - new MachineCoordinateProjectionMapping( - MappingKind, - 1.0, - 1.0, - 0.0, - 0.0)); - - public static string CreateProjectionId( - string projectId, - string cameraId, - string acquisitionId) - { - var identity = string.Join( - "\u001F", - RequireText(projectId, nameof(projectId)), - RequireText(cameraId, nameof(cameraId)), - RequireText(acquisitionId, nameof(acquisitionId))); - var bytes = System.Text.Encoding.UTF8.GetBytes(identity); - return $"projection-{Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bytes)).ToLowerInvariant()}"; - } - - public static void Validate(MachineCoordinateProjectionProfile profile) - { - ArgumentNullException.ThrowIfNull(profile); - if (!string.Equals(profile.SchemaVersion, SchemaVersion, StringComparison.Ordinal)) - { - throw new InvalidDataException( - $"Unsupported coordinate projection schema: '{profile.SchemaVersion}'."); - } - - RequireText(profile.ProjectionId, nameof(profile.ProjectionId)); - if (!string.IsNullOrWhiteSpace(profile.TwoDTransactionId) - && (!Guid.TryParse(profile.TwoDTransactionId, out var twoDTransactionId) - || twoDTransactionId == Guid.Empty)) - { - throw new InvalidDataException( - "A coordinate projection TwoD transaction identity must be a non-empty GUID when supplied."); - } - - ArgumentNullException.ThrowIfNull(profile.Image); - if (profile.Image.Width <= 1 || profile.Image.Height <= 1 - || !string.Equals(profile.Image.Unit, ImageUnit, StringComparison.Ordinal) - || !string.Equals(profile.Image.Origin, ImageOrigin, StringComparison.Ordinal)) - { - throw new InvalidDataException( - "Coordinate projection image dimensions, unit, and origin are invalid."); - } - - ArgumentNullException.ThrowIfNull(profile.Grid); - if (!string.Equals(profile.Grid.Unit, GridUnit, StringComparison.Ordinal) - || !string.Equals(profile.Grid.FrameId, GridFrameId, StringComparison.Ordinal) - || !string.Equals(profile.Grid.Origin, GridOrigin, StringComparison.Ordinal)) - { - throw new InvalidDataException( - "Coordinate projection grid unit, frame, and origin are invalid."); - } - - ArgumentNullException.ThrowIfNull(profile.Mapping); - if (!string.Equals(profile.Mapping.Kind, MappingKind, StringComparison.Ordinal) - || !double.IsFinite(profile.Mapping.ScaleX) - || !double.IsFinite(profile.Mapping.ScaleY) - || profile.Mapping.ScaleX == 0.0 - || profile.Mapping.ScaleY == 0.0 - || !double.IsFinite(profile.Mapping.OffsetX) - || !double.IsFinite(profile.Mapping.OffsetY)) - { - throw new InvalidDataException( - "Coordinate projection mapping must be normalized-linear with finite non-zero scale."); - } - } - - public static string SerializeProfile(MachineCoordinateProjectionProfile profile) - { - Validate(profile); - return JsonSerializer.Serialize(profile, JsonOptions); - } - - public static MachineCoordinateProjectionProfile ReadProfile(string path) - { - ArgumentException.ThrowIfNullOrWhiteSpace(path); - var profile = JsonSerializer.Deserialize( - File.ReadAllText(Path.GetFullPath(path)), - JsonOptions) - ?? throw new InvalidDataException("Coordinate projection profile is empty."); - Validate(profile); - return profile; - } - - public static string SerializeResult(MachineCoordinateProjectionResult result) - { - ValidateResult(result); - return JsonSerializer.Serialize(result, JsonOptions); - } - - public static MachineCoordinateProjectionResult ReadResult(string path) - { - ArgumentException.ThrowIfNullOrWhiteSpace(path); - var result = JsonSerializer.Deserialize( - File.ReadAllText(Path.GetFullPath(path)), - JsonOptions) - ?? throw new InvalidDataException("Coordinate projection result is empty."); - ValidateResult(result); - return result; - } - - public static (double X, double Y) MapImageToGrid( - MachineCoordinateProjectionProfile profile, - double imageX, - double imageY, - int gridWidth, - int gridHeight) - { - Validate(profile); - ValidateCoordinate(imageX, nameof(imageX)); - ValidateCoordinate(imageY, nameof(imageY)); - ValidateGridDimensions(gridWidth, gridHeight); - return ( - profile.Mapping.OffsetX - + imageX / (profile.Image.Width - 1) - * (gridWidth - 1) - * profile.Mapping.ScaleX, - profile.Mapping.OffsetY - + imageY / (profile.Image.Height - 1) - * (gridHeight - 1) - * profile.Mapping.ScaleY); - } - - public static (double X, double Y) MapGridToImage( - MachineCoordinateProjectionProfile profile, - double gridX, - double gridY, - int gridWidth, - int gridHeight) - { - Validate(profile); - ValidateCoordinate(gridX, nameof(gridX)); - ValidateCoordinate(gridY, nameof(gridY)); - ValidateGridDimensions(gridWidth, gridHeight); - return ( - (gridX - profile.Mapping.OffsetX) - / ((gridWidth - 1) * profile.Mapping.ScaleX) - * (profile.Image.Width - 1), - (gridY - profile.Mapping.OffsetY) - / ((gridHeight - 1) * profile.Mapping.ScaleY) - * (profile.Image.Height - 1)); - } - - private static void ValidateResult(MachineCoordinateProjectionResult result) - { - ArgumentNullException.ThrowIfNull(result); - if (!string.Equals(result.SchemaVersion, SchemaVersion, StringComparison.Ordinal) - || string.IsNullOrWhiteSpace(result.ProjectionId) - || !Guid.TryParse(result.TwoDTransactionId, out var twoDTransactionId) - || twoDTransactionId == Guid.Empty - || !Guid.TryParse(result.ThreeDTransactionId, out var threeDTransactionId) - || threeDTransactionId == Guid.Empty - || result.ImageWidth <= 1 - || result.ImageHeight <= 1 - || result.GridWidth <= 1 - || result.GridHeight <= 1 - || result.TwoDToThreeD is null - || result.ThreeDToTwoD is null) - { - throw new InvalidDataException("Coordinate projection result identity or dimensions are invalid."); - } - - ValidatePoints(result.TwoDToThreeD); - ValidatePoints(result.ThreeDToTwoD); - } - - private static void ValidatePoints( - IEnumerable points) - { - foreach (var point in points) - { - if (point is null - || string.IsNullOrWhiteSpace(point.Direction) - || string.IsNullOrWhiteSpace(point.Id) - || !double.IsFinite(point.ImageX) - || !double.IsFinite(point.ImageY) - || !double.IsFinite(point.GridX) - || !double.IsFinite(point.GridY) - || point.SampledHeight is { } height && !double.IsFinite(height)) - { - throw new InvalidDataException("Coordinate projection contains an invalid point."); - } - } - } - - private static void ValidateGridDimensions(int gridWidth, int gridHeight) - { - if (gridWidth <= 1 || gridHeight <= 1) - { - throw new ArgumentOutOfRangeException( - nameof(gridWidth), - "Projection requires a grid wider and taller than one cell."); - } - } - - private static void ValidateCoordinate(double value, string parameterName) - { - if (!double.IsFinite(value)) - { - throw new ArgumentOutOfRangeException(parameterName, value, "Coordinate must be finite."); - } - } - - private static string RequireText(string value, string parameterName) => - string.IsNullOrWhiteSpace(value) - ? throw new ArgumentException("A non-empty value is required.", parameterName) - : value.Trim(); -} - -public sealed record MachineCoordinateProjectionProfile( - string SchemaVersion, - string ProjectionId, - string? TwoDTransactionId, - MachineCoordinateProjectionImage Image, - MachineCoordinateProjectionGrid Grid, - MachineCoordinateProjectionMapping Mapping); - -public sealed record MachineCoordinateProjectionImage( - int Width, - int Height, - string Unit, - string Origin); - -public sealed record MachineCoordinateProjectionGrid( - string Unit, - string FrameId, - string Origin); - -public sealed record MachineCoordinateProjectionMapping( - string Kind, - double ScaleX, - double ScaleY, - double OffsetX, - double OffsetY); - -public sealed record MachineCoordinateProjectionResult( - string SchemaVersion, - string ProjectionId, - string TwoDTransactionId, - string ThreeDTransactionId, - string Outcome, - string TwoDRunId, - string ThreeDRunId, - int ImageWidth, - int ImageHeight, - int GridWidth, - int GridHeight, - IReadOnlyList TwoDToThreeD, - IReadOnlyList ThreeDToTwoD, - DateTimeOffset RecordedAtUtc); - -public sealed record MachineProjectedCoordinate( - string Direction, - string Id, - string Kind, - string Label, - double ImageX, - double ImageY, - double GridX, - double GridY, - double? SampledHeight, - string SampleStatus, - string InspectionStatus); diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionContract.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionContract.cs new file mode 100644 index 0000000..ce8968c --- /dev/null +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionContract.cs @@ -0,0 +1,156 @@ +namespace OpenVisionLab.Machine.Infrastructure.Integration; + +/// +/// Stable sidecar contract for the first cross-modal projection slice. The +/// profile describes software coordinates only: image pixels and C3D +/// grid-index coordinates both use a top-left origin. +/// +public static class MachineCoordinateProjectionContract +{ + public const string SchemaVersion = "1.0"; + public const string ProfileArtifactRole = "coordinate-projection-profile"; + public const string ProfileArtifactId = "coordinate-projection-profile"; + public const string ResultEvidenceRole = "coordinate-projection-result"; + public const string ResultEvidenceArtifactId = "coordinate-projection-result"; + public const string MappingKind = "normalized-linear"; + public const string ImageUnit = "px"; + public const string ImageOrigin = "top-left"; + public const string GridUnit = "raw-height"; + public const string GridFrameId = "frame.c3d-grid-index"; + public const string GridOrigin = "top-left"; + + public static MachineCoordinateProjectionProfile CreateDefault( + string projectionId, + int imageWidth, + int imageHeight, + string? twoDTransactionId = null) => + new( + SchemaVersion, + MachineCoordinateProjectionValidator.RequireText(projectionId, nameof(projectionId)), + twoDTransactionId, + new MachineCoordinateProjectionImage( + imageWidth, + imageHeight, + ImageUnit, + ImageOrigin), + new MachineCoordinateProjectionGrid( + GridUnit, + GridFrameId, + GridOrigin), + new MachineCoordinateProjectionMapping( + MappingKind, + 1.0, + 1.0, + 0.0, + 0.0)); + + public static string CreateProjectionId( + string projectId, + string cameraId, + string acquisitionId) + { + var identity = string.Join( + "\u001F", + MachineCoordinateProjectionValidator.RequireText(projectId, nameof(projectId)), + MachineCoordinateProjectionValidator.RequireText(cameraId, nameof(cameraId)), + MachineCoordinateProjectionValidator.RequireText(acquisitionId, nameof(acquisitionId))); + var bytes = System.Text.Encoding.UTF8.GetBytes(identity); + return $"projection-{Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bytes)).ToLowerInvariant()}"; + } + + public static void Validate(MachineCoordinateProjectionProfile profile) => + MachineCoordinateProjectionValidator.ValidateProfile(profile); + + public static string SerializeProfile(MachineCoordinateProjectionProfile profile) => + MachineCoordinateProjectionPersistence.SerializeProfile(profile); + + public static MachineCoordinateProjectionProfile ReadProfile(string path) => + MachineCoordinateProjectionPersistence.ReadProfile(path); + + public static string SerializeResult(MachineCoordinateProjectionResult result) => + MachineCoordinateProjectionPersistence.SerializeResult(result); + + public static MachineCoordinateProjectionResult ReadResult(string path) => + MachineCoordinateProjectionPersistence.ReadResult(path); + + public static (double X, double Y) MapImageToGrid( + MachineCoordinateProjectionProfile profile, + double imageX, + double imageY, + int gridWidth, + int gridHeight) + => MachineCoordinateProjectionMapper.MapImageToGrid( + profile, + imageX, + imageY, + gridWidth, + gridHeight); + + public static (double X, double Y) MapGridToImage( + MachineCoordinateProjectionProfile profile, + double gridX, + double gridY, + int gridWidth, + int gridHeight) + => MachineCoordinateProjectionMapper.MapGridToImage( + profile, + gridX, + gridY, + gridWidth, + gridHeight); +} + +public sealed record MachineCoordinateProjectionProfile( + string SchemaVersion, + string ProjectionId, + string? TwoDTransactionId, + MachineCoordinateProjectionImage Image, + MachineCoordinateProjectionGrid Grid, + MachineCoordinateProjectionMapping Mapping); + +public sealed record MachineCoordinateProjectionImage( + int Width, + int Height, + string Unit, + string Origin); + +public sealed record MachineCoordinateProjectionGrid( + string Unit, + string FrameId, + string Origin); + +public sealed record MachineCoordinateProjectionMapping( + string Kind, + double ScaleX, + double ScaleY, + double OffsetX, + double OffsetY); + +public sealed record MachineCoordinateProjectionResult( + string SchemaVersion, + string ProjectionId, + string TwoDTransactionId, + string ThreeDTransactionId, + string Outcome, + string TwoDRunId, + string ThreeDRunId, + int ImageWidth, + int ImageHeight, + int GridWidth, + int GridHeight, + IReadOnlyList TwoDToThreeD, + IReadOnlyList ThreeDToTwoD, + DateTimeOffset RecordedAtUtc); + +public sealed record MachineProjectedCoordinate( + string Direction, + string Id, + string Kind, + string Label, + double ImageX, + double ImageY, + double GridX, + double GridY, + double? SampledHeight, + string SampleStatus, + string InspectionStatus); diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionMapper.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionMapper.cs new file mode 100644 index 0000000..0c2d320 --- /dev/null +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionMapper.cs @@ -0,0 +1,50 @@ +namespace OpenVisionLab.Machine.Infrastructure.Integration; + +/// +/// Applies the normalized-linear image/grid mapping policy to validated +/// coordinate projection profiles. +/// +internal static class MachineCoordinateProjectionMapper +{ + internal static (double X, double Y) MapImageToGrid( + MachineCoordinateProjectionProfile profile, + double imageX, + double imageY, + int gridWidth, + int gridHeight) + { + MachineCoordinateProjectionValidator.ValidateProfile(profile); + MachineCoordinateProjectionValidator.ValidateCoordinate(imageX, nameof(imageX)); + MachineCoordinateProjectionValidator.ValidateCoordinate(imageY, nameof(imageY)); + MachineCoordinateProjectionValidator.ValidateGridDimensions(gridWidth, gridHeight); + return ( + profile.Mapping.OffsetX + + imageX / (profile.Image.Width - 1) + * (gridWidth - 1) + * profile.Mapping.ScaleX, + profile.Mapping.OffsetY + + imageY / (profile.Image.Height - 1) + * (gridHeight - 1) + * profile.Mapping.ScaleY); + } + + internal static (double X, double Y) MapGridToImage( + MachineCoordinateProjectionProfile profile, + double gridX, + double gridY, + int gridWidth, + int gridHeight) + { + MachineCoordinateProjectionValidator.ValidateProfile(profile); + MachineCoordinateProjectionValidator.ValidateCoordinate(gridX, nameof(gridX)); + MachineCoordinateProjectionValidator.ValidateCoordinate(gridY, nameof(gridY)); + MachineCoordinateProjectionValidator.ValidateGridDimensions(gridWidth, gridHeight); + return ( + (gridX - profile.Mapping.OffsetX) + / ((gridWidth - 1) * profile.Mapping.ScaleX) + * (profile.Image.Width - 1), + (gridY - profile.Mapping.OffsetY) + / ((gridHeight - 1) * profile.Mapping.ScaleY) + * (profile.Image.Height - 1)); + } +} diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionPersistence.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionPersistence.cs new file mode 100644 index 0000000..42c53db --- /dev/null +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionPersistence.cs @@ -0,0 +1,51 @@ +using System.Text.Json; + +namespace OpenVisionLab.Machine.Infrastructure.Integration; + +/// +/// Owns JSON serialization and path-based persistence for coordinate +/// projection contracts. +/// +internal static class MachineCoordinateProjectionPersistence +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + internal static string SerializeProfile(MachineCoordinateProjectionProfile profile) + { + MachineCoordinateProjectionValidator.ValidateProfile(profile); + return JsonSerializer.Serialize(profile, JsonOptions); + } + + internal static MachineCoordinateProjectionProfile ReadProfile(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var profile = JsonSerializer.Deserialize( + File.ReadAllText(Path.GetFullPath(path)), + JsonOptions) + ?? throw new InvalidDataException("Coordinate projection profile is empty."); + MachineCoordinateProjectionValidator.ValidateProfile(profile); + return profile; + } + + internal static string SerializeResult(MachineCoordinateProjectionResult result) + { + MachineCoordinateProjectionValidator.ValidateResult(result); + return JsonSerializer.Serialize(result, JsonOptions); + } + + internal static MachineCoordinateProjectionResult ReadResult(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var result = JsonSerializer.Deserialize( + File.ReadAllText(Path.GetFullPath(path)), + JsonOptions) + ?? throw new InvalidDataException("Coordinate projection result is empty."); + MachineCoordinateProjectionValidator.ValidateResult(result); + return result; + } +} diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionValidator.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionValidator.cs new file mode 100644 index 0000000..184e50b --- /dev/null +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Projection/MachineCoordinateProjectionValidator.cs @@ -0,0 +1,122 @@ +namespace OpenVisionLab.Machine.Infrastructure.Integration; + +/// +/// Validates the persisted coordinate projection contracts without performing +/// mapping or file I/O. +/// +internal static class MachineCoordinateProjectionValidator +{ + internal static void ValidateProfile(MachineCoordinateProjectionProfile profile) + { + ArgumentNullException.ThrowIfNull(profile); + if (!string.Equals(profile.SchemaVersion, MachineCoordinateProjectionContract.SchemaVersion, StringComparison.Ordinal)) + { + throw new InvalidDataException( + $"Unsupported coordinate projection schema: '{profile.SchemaVersion}'."); + } + + RequireText(profile.ProjectionId, nameof(profile.ProjectionId)); + if (!string.IsNullOrWhiteSpace(profile.TwoDTransactionId) + && (!Guid.TryParse(profile.TwoDTransactionId, out var twoDTransactionId) + || twoDTransactionId == Guid.Empty)) + { + throw new InvalidDataException( + "A coordinate projection TwoD transaction identity must be a non-empty GUID when supplied."); + } + + ArgumentNullException.ThrowIfNull(profile.Image); + if (profile.Image.Width <= 1 || profile.Image.Height <= 1 + || !string.Equals(profile.Image.Unit, MachineCoordinateProjectionContract.ImageUnit, StringComparison.Ordinal) + || !string.Equals(profile.Image.Origin, MachineCoordinateProjectionContract.ImageOrigin, StringComparison.Ordinal)) + { + throw new InvalidDataException( + "Coordinate projection image dimensions, unit, and origin are invalid."); + } + + ArgumentNullException.ThrowIfNull(profile.Grid); + if (!string.Equals(profile.Grid.Unit, MachineCoordinateProjectionContract.GridUnit, StringComparison.Ordinal) + || !string.Equals(profile.Grid.FrameId, MachineCoordinateProjectionContract.GridFrameId, StringComparison.Ordinal) + || !string.Equals(profile.Grid.Origin, MachineCoordinateProjectionContract.GridOrigin, StringComparison.Ordinal)) + { + throw new InvalidDataException( + "Coordinate projection grid unit, frame, and origin are invalid."); + } + + ArgumentNullException.ThrowIfNull(profile.Mapping); + if (!string.Equals(profile.Mapping.Kind, MachineCoordinateProjectionContract.MappingKind, StringComparison.Ordinal) + || !double.IsFinite(profile.Mapping.ScaleX) + || !double.IsFinite(profile.Mapping.ScaleY) + || profile.Mapping.ScaleX == 0.0 + || profile.Mapping.ScaleY == 0.0 + || !double.IsFinite(profile.Mapping.OffsetX) + || !double.IsFinite(profile.Mapping.OffsetY)) + { + throw new InvalidDataException( + "Coordinate projection mapping must be normalized-linear with finite non-zero scale."); + } + } + + internal static void ValidateResult(MachineCoordinateProjectionResult result) + { + ArgumentNullException.ThrowIfNull(result); + if (!string.Equals(result.SchemaVersion, MachineCoordinateProjectionContract.SchemaVersion, StringComparison.Ordinal) + || string.IsNullOrWhiteSpace(result.ProjectionId) + || !Guid.TryParse(result.TwoDTransactionId, out var twoDTransactionId) + || twoDTransactionId == Guid.Empty + || !Guid.TryParse(result.ThreeDTransactionId, out var threeDTransactionId) + || threeDTransactionId == Guid.Empty + || result.ImageWidth <= 1 + || result.ImageHeight <= 1 + || result.GridWidth <= 1 + || result.GridHeight <= 1 + || result.TwoDToThreeD is null + || result.ThreeDToTwoD is null) + { + throw new InvalidDataException("Coordinate projection result identity or dimensions are invalid."); + } + + ValidatePoints(result.TwoDToThreeD); + ValidatePoints(result.ThreeDToTwoD); + } + + internal static void ValidateGridDimensions(int gridWidth, int gridHeight) + { + if (gridWidth <= 1 || gridHeight <= 1) + { + throw new ArgumentOutOfRangeException( + nameof(gridWidth), + "Projection requires a grid wider and taller than one cell."); + } + } + + internal static void ValidateCoordinate(double value, string parameterName) + { + if (!double.IsFinite(value)) + { + throw new ArgumentOutOfRangeException(parameterName, value, "Coordinate must be finite."); + } + } + + internal static string RequireText(string value, string parameterName) => + string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("A non-empty value is required.", parameterName) + : value.Trim(); + + private static void ValidatePoints(IEnumerable points) + { + foreach (var point in points) + { + if (point is null + || string.IsNullOrWhiteSpace(point.Direction) + || string.IsNullOrWhiteSpace(point.Id) + || !double.IsFinite(point.ImageX) + || !double.IsFinite(point.ImageY) + || !double.IsFinite(point.GridX) + || !double.IsFinite(point.GridY) + || point.SampledHeight is { } height && !double.IsFinite(height)) + { + throw new InvalidDataException("Coordinate projection contains an invalid point."); + } + } + } +} diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTransactionFileSystem.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionFileSystem.cs similarity index 100% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTransactionFileSystem.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionFileSystem.cs diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTransactionMaintenance.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionInspector.cs similarity index 63% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTransactionMaintenance.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionInspector.cs index 0253c9f..bb78e75 100644 --- a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationTransactionMaintenance.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionInspector.cs @@ -4,10 +4,10 @@ namespace OpenVisionLab.Machine.Infrastructure.Integration; /// -/// Owns discovery, diagnostics, stale staging cleanup, and quarantine -/// retention for Machine Integration transactions. +/// Owns read-only discovery, diagnostics, and safe observation of Machine +/// Integration transaction directories and messages. /// -internal static class MachineIntegrationTransactionMaintenance +internal static class MachineIntegrationTransactionInspector { private const string QuarantineDirectoryName = ".quarantine"; private const string QuarantineManifestFileName = "quarantine.json"; @@ -125,177 +125,6 @@ internal static IReadOnlyList DiagnoseT .ToArray(); } - internal static MachineIntegrationCleanupReport CleanupStaging( - string exchangeRoot, - TimeSpan staleAfter, - DateTimeOffset? nowUtc = null, - CancellationToken cancellationToken = default) - { - if (staleAfter < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException( - nameof(staleAfter), - "The staging retention period cannot be negative."); - } - - var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); - var transactionsRoot = Path.Combine( - root, - IntegrationTransactionLayout.TransactionsDirectoryName); - if (!Directory.Exists(transactionsRoot)) - { - return new(0, 0, []); - } - - MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(root); - MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(transactionsRoot); - var cutoff = (nowUtc ?? DateTimeOffset.UtcNow).ToUniversalTime() - staleAfter; - var scanned = 0; - var quarantined = 0; - foreach (var directory in Directory.EnumerateDirectories(transactionsRoot)) - { - cancellationToken.ThrowIfCancellationRequested(); - if (!TryParseStagingDirectoryName( - Path.GetFileName(directory), - out var transactionId)) - { - continue; - } - - scanned++; - if (MachineIntegrationTransactionFileSystem.GetLastWriteTimeUtc(directory) > cutoff) - { - continue; - } - - var stagedHandoff = TryReadStagedHandoff(directory); - if (TryQuarantineStagingDirectory( - transactionsRoot, - directory, - stagedHandoff, - "stale-staging", - null, - null, - GetReferencedArtifactBytes( - directory, - stagedHandoff?.Context.Artifacts), - transactionId)) - { - quarantined++; - } - } - - return new( - scanned, - quarantined, - DiagnoseTransactions(root)); - } - - internal static int PurgeQuarantine( - string exchangeRoot, - TimeSpan retention, - DateTimeOffset? nowUtc = null, - CancellationToken cancellationToken = default) - { - if (retention < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException( - nameof(retention), - "The quarantine retention period cannot be negative."); - } - - var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); - var quarantineRoot = MachineIntegrationTransactionFileSystem.GetQuarantineDirectory(root); - if (!Directory.Exists(quarantineRoot)) - { - return 0; - } - - MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(root); - MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(quarantineRoot); - var cutoff = (nowUtc ?? DateTimeOffset.UtcNow).ToUniversalTime() - retention; - var purged = 0; - foreach (var directory in Directory.EnumerateDirectories(quarantineRoot)) - { - cancellationToken.ThrowIfCancellationRequested(); - if (MachineIntegrationTransactionFileSystem.GetLastWriteTimeUtc(directory) > cutoff) - { - continue; - } - - MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(directory); - Directory.Delete(directory, recursive: true); - purged++; - } - - return purged; - } - - internal static bool TryQuarantineStagingDirectory( - string transactionsRoot, - string stagingDirectory, - IntegrationHandoffV2? handoff, - string reason, - Exception? exception, - IProgress? progress, - long materializedBytes, - Guid? transactionIdOverride = null) - { - if (!Directory.Exists(stagingDirectory)) - { - return false; - } - - var transactionId = handoff?.TransactionId - ?? transactionIdOverride - ?? (TryParseStagingDirectoryName( - Path.GetFileName(stagingDirectory), - out var parsedId) - ? parsedId - : Guid.NewGuid()); - var quarantineRoot = Path.Combine( - transactionsRoot, - QuarantineDirectoryName); - try - { - Directory.CreateDirectory(quarantineRoot); - MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(quarantineRoot); - var quarantineDirectory = Path.Combine( - quarantineRoot, - $"{transactionId:D}.{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}.{Guid.NewGuid():N}"); - Directory.Move(stagingDirectory, quarantineDirectory); - - var declaredBytes = handoff is null - ? 0 - : MachineIntegrationTransactionFileSystem.GetDeclaredArtifactBytes(handoff.Context.Artifacts); - var manifest = new QuarantineManifest( - transactionId, - reason, - exception?.GetType().FullName, - exception?.Message, - DateTimeOffset.UtcNow, - handoff?.Context.Artifacts.Count ?? 0, - declaredBytes, - materializedBytes); - TryWriteQuarantineManifest(quarantineDirectory, manifest); - TryReportProgress( - progress, - transactionId, - MachineIntegrationTransferPhase.Quarantined, - null, - materializedBytes, - declaredBytes, - 0, - manifest.ArtifactCount); - return true; - } - catch - { - // Keep the original publish or cleanup failure and leave staging in place for diagnosis. - return false; - } - } - private static MachineIntegrationTransactionDiagnostic DiagnosePublishedDirectory( string root, string directory, @@ -409,12 +238,88 @@ private static MachineIntegrationTransactionDiagnostic CreateDiagnostic( availableFreeBytes, detail); - private static IntegrationHandoffV2 ReadHandoffEnvelope( + internal static IntegrationHandoffV2 ReadHandoff( + string exchangeRoot, + Guid transactionId) + { + var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); + var handoff = ReadHandoffEnvelope(root, transactionId); + var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory(root, transactionId); + foreach (var artifact in handoff.Context.Artifacts) + { + MachineIntegrationTransactionFileSystem.EnsureNoReparsePoints(transactionDirectory, artifact.RelativePath); + ThrowIfInvalid(IntegrationContractValidator.ValidateArtifactFile( + artifact, + transactionDirectory)); + } + + return handoff; + } + + internal static IntegrationAcknowledgementV2 ReadAcknowledgement( + string exchangeRoot, + Guid transactionId) + { + var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); + var handoff = ReadHandoff(root, transactionId); + var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory(root, transactionId); + var acknowledgement = IntegrationContractJson.DeserializeAcknowledgementV2( + MachineIntegrationTransactionFileSystem.ReadMessage( + transactionDirectory, + IntegrationTransactionLayout.AcknowledgementFileName)); + ThrowIfInvalid(IntegrationContractValidator.ValidateV2Sequence( + handoff, + acknowledgement)); + return acknowledgement; + } + + internal static IntegrationResultV2 ReadResult( + string exchangeRoot, + Guid transactionId) + { + var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); + var handoff = ReadHandoff(root, transactionId); + var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory(root, transactionId); + var acknowledgement = IntegrationContractJson.DeserializeAcknowledgementV2( + MachineIntegrationTransactionFileSystem.ReadMessage( + transactionDirectory, + IntegrationTransactionLayout.AcknowledgementFileName)); + var result = IntegrationContractJson.DeserializeResultV2( + MachineIntegrationTransactionFileSystem.ReadMessage( + transactionDirectory, + IntegrationTransactionLayout.ResultFileName)); + ThrowIfInvalid(IntegrationContractValidator.ValidateV2Sequence( + handoff, + acknowledgement, + result)); + + if (result.RunRecord is not null) + { + MachineIntegrationTransactionFileSystem.EnsureNoReparsePoints( + transactionDirectory, + result.RunRecord.RelativePath); + ThrowIfInvalid(IntegrationContractValidator.ValidateArtifactFile( + result.RunRecord, + transactionDirectory)); + } + foreach (var evidence in result.Evidence) + { + MachineIntegrationTransactionFileSystem.EnsureNoReparsePoints(transactionDirectory, evidence.RelativePath); + ThrowIfInvalid(IntegrationContractValidator.ValidateArtifactFile( + evidence, + transactionDirectory)); + } + + return result; + } + + internal static IntegrationHandoffV2 ReadHandoffEnvelope( string exchangeRoot, Guid transactionId) { + var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory( - exchangeRoot, + root, transactionId); var handoff = IntegrationContractJson.DeserializeHandoffV2( MachineIntegrationTransactionFileSystem.ReadMessage( @@ -430,7 +335,7 @@ private static IntegrationHandoffV2 ReadHandoffEnvelope( return handoff; } - private static IntegrationHandoffV2? TryReadStagedHandoff(string directory) + internal static IntegrationHandoffV2? TryReadStagedHandoff(string directory) { try { @@ -447,13 +352,13 @@ private static IntegrationHandoffV2 ReadHandoffEnvelope( } } - private static QuarantineManifest? TryReadQuarantineManifest(string directory) + private static MachineIntegrationQuarantineManifest? TryReadQuarantineManifest(string directory) { try { var path = Path.Combine(directory, QuarantineManifestFileName); return File.Exists(path) - ? JsonSerializer.Deserialize(File.ReadAllBytes(path)) + ? JsonSerializer.Deserialize(File.ReadAllBytes(path)) : null; } catch @@ -462,7 +367,7 @@ private static IntegrationHandoffV2 ReadHandoffEnvelope( } } - private static long GetReferencedArtifactBytes( + internal static long GetReferencedArtifactBytes( string transactionDirectory, IReadOnlyList? artifacts) { @@ -527,7 +432,7 @@ private static long GetTotalFileBytes(string directory) return total; } - private static bool TryParseStagingDirectoryName( + internal static bool TryParseStagingDirectoryName( string? name, out Guid transactionId) { @@ -560,55 +465,12 @@ private static bool TryParseQuarantineDirectoryName( && Guid.TryParseExact(name[..36], "D", out transactionId); } - private static void TryWriteQuarantineManifest( - string quarantineDirectory, - QuarantineManifest manifest) - { - try - { - MachineIntegrationTransactionFileSystem.WriteMessage( - Path.Combine(quarantineDirectory, QuarantineManifestFileName), - JsonSerializer.SerializeToUtf8Bytes(manifest)); - } - catch - { - // The quarantined bytes remain the primary evidence if the manifest cannot be written. - } - } - - private static void TryReportProgress( - IProgress? progress, - Guid transactionId, - MachineIntegrationTransferPhase phase, - string? artifactId, - long bytesCopied, - long totalBytes, - int completedArtifacts, - int artifactCount) - { - try - { - progress?.Report(new( - transactionId, - phase, - artifactId, - bytesCopied, - totalBytes, - completedArtifacts, - artifactCount)); - } - catch - { - // A completion observer cannot turn an already-published transaction into a failure. - } - } - private static string DescribeException(Exception exception) => string.IsNullOrWhiteSpace(exception.Message) ? exception.GetType().Name : $"{exception.GetType().Name}: {exception.Message}"; - private static string RequireText(string value, string parameterName) => + internal static string RequireText(string value, string parameterName) => string.IsNullOrWhiteSpace(value) ? throw new ArgumentException("A non-empty path is required.", parameterName) : value.Trim(); @@ -625,14 +487,14 @@ private static void ThrowIfInvalid(IntegrationValidationResult validation) issue.Code, $"{issue.Field}: {issue.Message}"); } - - private sealed record QuarantineManifest( - Guid TransactionId, - string Reason, - string? ExceptionType, - string? Message, - DateTimeOffset QuarantinedAtUtc, - int ArtifactCount, - long DeclaredBytes, - long MaterializedBytes); } + +internal sealed record MachineIntegrationQuarantineManifest( + Guid TransactionId, + string Reason, + string? ExceptionType, + string? Message, + DateTimeOffset QuarantinedAtUtc, + int ArtifactCount, + long DeclaredBytes, + long MaterializedBytes); diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationExchange.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionPublisher.cs similarity index 70% rename from src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationExchange.cs rename to src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionPublisher.cs index 0311e72..f81e2d2 100644 --- a/src/OpenVisionLab.Machine.Infrastructure/Integration/MachineIntegrationExchange.cs +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionPublisher.cs @@ -2,23 +2,16 @@ namespace OpenVisionLab.Machine.Infrastructure.Integration; -public sealed record MachineIntegrationTransactionSummary( - IntegrationHandoffV2 Handoff, - bool HasAcknowledgement, - bool HasResult); - /// -/// Owns the Machine Studio side of the v2 file exchange. A Handoff is staged, -/// copied artifacts are checked against their declared identity, and the -/// complete transaction directory is published with one directory move. -/// Reading and importing never starts a simulation, loads a recipe, or runs an -/// inspection. +/// Owns v2 transaction handoff publication: validation, staging, artifact copying, +/// atomic directory publication, progress, and failed-staging quarantine. +/// Read-only transaction inspection remains with the transaction inspector. /// -public static class MachineIntegrationExchange +internal static class MachineIntegrationTransactionPublisher { - public const long DefaultMinimumFreeSpaceBytes = 1_048_576; + internal const long DefaultMinimumFreeSpaceBytes = 1_048_576; - public static IntegrationHandoffV2 PublishHandoff( + internal static IntegrationHandoffV2 PublishHandoff( string exchangeRoot, IntegrationHandoffV2 handoff, IReadOnlyDictionary artifactSourcePaths) => @@ -29,7 +22,7 @@ public static IntegrationHandoffV2 PublishHandoff( .GetAwaiter() .GetResult(); - public static async Task PublishHandoffAsync( + internal static async Task PublishHandoffAsync( string exchangeRoot, IntegrationHandoffV2 handoff, IReadOnlyDictionary artifactSourcePaths, @@ -187,7 +180,7 @@ public static async Task PublishHandoffAsync( } catch (Exception exception) { - MachineIntegrationTransactionMaintenance.TryQuarantineStagingDirectory( + MachineIntegrationTransactionRetention.TryQuarantineStagingDirectory( transactionsRoot, stagingDirectory, handoff, @@ -201,132 +194,6 @@ exception is OperationCanceledException } } - public static IReadOnlyList DiscoverTransactions( - string exchangeRoot) => - MachineIntegrationTransactionMaintenance.DiscoverTransactions(exchangeRoot); - - public static IntegrationHandoffV2 ReadHandoff( - string exchangeRoot, - Guid transactionId) - { - var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); - var handoff = ReadHandoffEnvelope(root, transactionId); - var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory(root, transactionId); - foreach (var artifact in handoff.Context.Artifacts) - { - MachineIntegrationTransactionFileSystem.EnsureNoReparsePoints(transactionDirectory, artifact.RelativePath); - ThrowIfInvalid(IntegrationContractValidator.ValidateArtifactFile( - artifact, - transactionDirectory)); - } - - return handoff; - } - - public static IntegrationHandoffV2 ReadHandoffEnvelope( - string exchangeRoot, - Guid transactionId) - { - var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory( - Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))), - transactionId); - var handoff = IntegrationContractJson.DeserializeHandoffV2( - MachineIntegrationTransactionFileSystem.ReadMessage( - transactionDirectory, - IntegrationTransactionLayout.HandoffFileName)); - if (handoff.TransactionId != transactionId) - { - throw new IntegrationContractException( - IntegrationErrorCode.CorrelationMismatch, - "Handoff transaction identity does not match its directory."); - } - - return handoff; - } - - public static IntegrationAcknowledgementV2 ReadAcknowledgement( - string exchangeRoot, - Guid transactionId) - { - var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); - var handoff = ReadHandoff(root, transactionId); - var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory(root, transactionId); - var acknowledgement = IntegrationContractJson.DeserializeAcknowledgementV2( - MachineIntegrationTransactionFileSystem.ReadMessage( - transactionDirectory, - IntegrationTransactionLayout.AcknowledgementFileName)); - ThrowIfInvalid(IntegrationContractValidator.ValidateV2Sequence( - handoff, - acknowledgement)); - return acknowledgement; - } - - public static IntegrationResultV2 ReadResult( - string exchangeRoot, - Guid transactionId) - { - var root = Path.GetFullPath(RequireText(exchangeRoot, nameof(exchangeRoot))); - var handoff = ReadHandoff(root, transactionId); - var transactionDirectory = MachineIntegrationTransactionFileSystem.GetTransactionDirectory(root, transactionId); - var acknowledgement = IntegrationContractJson.DeserializeAcknowledgementV2( - MachineIntegrationTransactionFileSystem.ReadMessage( - transactionDirectory, - IntegrationTransactionLayout.AcknowledgementFileName)); - var result = IntegrationContractJson.DeserializeResultV2( - MachineIntegrationTransactionFileSystem.ReadMessage( - transactionDirectory, - IntegrationTransactionLayout.ResultFileName)); - ThrowIfInvalid(IntegrationContractValidator.ValidateV2Sequence( - handoff, - acknowledgement, - result)); - - if (result.RunRecord is not null) - { - MachineIntegrationTransactionFileSystem.EnsureNoReparsePoints( - transactionDirectory, - result.RunRecord.RelativePath); - ThrowIfInvalid(IntegrationContractValidator.ValidateArtifactFile( - result.RunRecord, - transactionDirectory)); - } - foreach (var evidence in result.Evidence) - { - MachineIntegrationTransactionFileSystem.EnsureNoReparsePoints(transactionDirectory, evidence.RelativePath); - ThrowIfInvalid(IntegrationContractValidator.ValidateArtifactFile( - evidence, - transactionDirectory)); - } - - return result; - } - - public static IReadOnlyList DiagnoseTransactions( - string exchangeRoot) => - MachineIntegrationTransactionMaintenance.DiagnoseTransactions(exchangeRoot); - - public static MachineIntegrationCleanupReport CleanupStaging( - string exchangeRoot, - TimeSpan staleAfter, - DateTimeOffset? nowUtc = null, - CancellationToken cancellationToken = default) => - MachineIntegrationTransactionMaintenance.CleanupStaging( - exchangeRoot, - staleAfter, - nowUtc, - cancellationToken); - - public static int PurgeQuarantine( - string exchangeRoot, - TimeSpan retention, - DateTimeOffset? nowUtc = null, - CancellationToken cancellationToken = default) => - MachineIntegrationTransactionMaintenance.PurgeQuarantine( - exchangeRoot, - retention, - nowUtc, - cancellationToken); - private static async Task CopyArtifactAsync( string sourcePath, string transactionDirectory, diff --git a/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionRetention.cs b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionRetention.cs new file mode 100644 index 0000000..997a07a --- /dev/null +++ b/src/OpenVisionLab.Machine.Infrastructure/Integration/Transactions/MachineIntegrationTransactionRetention.cs @@ -0,0 +1,230 @@ +using System.Text.Json; +using OpenVisionLab.Integration.Contracts; + +namespace OpenVisionLab.Machine.Infrastructure.Integration; + +/// +/// Owns mutating staging and quarantine retention operations for Machine +/// Integration transactions. +/// +internal static class MachineIntegrationTransactionRetention +{ + private const string QuarantineDirectoryName = ".quarantine"; + private const string QuarantineManifestFileName = "quarantine.json"; + + internal static MachineIntegrationCleanupReport CleanupStaging( + string exchangeRoot, + TimeSpan staleAfter, + DateTimeOffset? nowUtc = null, + CancellationToken cancellationToken = default) + { + if (staleAfter < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException( + nameof(staleAfter), + "The staging retention period cannot be negative."); + } + + var root = Path.GetFullPath( + MachineIntegrationTransactionInspector.RequireText(exchangeRoot, nameof(exchangeRoot))); + var transactionsRoot = Path.Combine( + root, + IntegrationTransactionLayout.TransactionsDirectoryName); + if (!Directory.Exists(transactionsRoot)) + { + return new(0, 0, []); + } + + MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(root); + MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(transactionsRoot); + var cutoff = (nowUtc ?? DateTimeOffset.UtcNow).ToUniversalTime() - staleAfter; + var scanned = 0; + var quarantined = 0; + foreach (var directory in Directory.EnumerateDirectories(transactionsRoot)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!MachineIntegrationTransactionInspector.TryParseStagingDirectoryName( + Path.GetFileName(directory), + out var transactionId)) + { + continue; + } + + scanned++; + if (MachineIntegrationTransactionFileSystem.GetLastWriteTimeUtc(directory) > cutoff) + { + continue; + } + + var stagedHandoff = MachineIntegrationTransactionInspector.TryReadStagedHandoff(directory); + if (TryQuarantineStagingDirectory( + transactionsRoot, + directory, + stagedHandoff, + "stale-staging", + null, + null, + MachineIntegrationTransactionInspector.GetReferencedArtifactBytes( + directory, + stagedHandoff?.Context.Artifacts), + transactionId)) + { + quarantined++; + } + } + + return new( + scanned, + quarantined, + MachineIntegrationTransactionInspector.DiagnoseTransactions(root)); + } + + internal static int PurgeQuarantine( + string exchangeRoot, + TimeSpan retention, + DateTimeOffset? nowUtc = null, + CancellationToken cancellationToken = default) + { + if (retention < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException( + nameof(retention), + "The quarantine retention period cannot be negative."); + } + + var root = Path.GetFullPath( + MachineIntegrationTransactionInspector.RequireText(exchangeRoot, nameof(exchangeRoot))); + var quarantineRoot = MachineIntegrationTransactionFileSystem.GetQuarantineDirectory(root); + if (!Directory.Exists(quarantineRoot)) + { + return 0; + } + + MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(root); + MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(quarantineRoot); + var cutoff = (nowUtc ?? DateTimeOffset.UtcNow).ToUniversalTime() - retention; + var purged = 0; + foreach (var directory in Directory.EnumerateDirectories(quarantineRoot)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (MachineIntegrationTransactionFileSystem.GetLastWriteTimeUtc(directory) > cutoff) + { + continue; + } + + MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(directory); + Directory.Delete(directory, recursive: true); + purged++; + } + + return purged; + } + + internal static bool TryQuarantineStagingDirectory( + string transactionsRoot, + string stagingDirectory, + IntegrationHandoffV2? handoff, + string reason, + Exception? exception, + IProgress? progress, + long materializedBytes, + Guid? transactionIdOverride = null) + { + if (!Directory.Exists(stagingDirectory)) + { + return false; + } + + var transactionId = handoff?.TransactionId + ?? transactionIdOverride + ?? (MachineIntegrationTransactionInspector.TryParseStagingDirectoryName( + Path.GetFileName(stagingDirectory), + out var parsedId) + ? parsedId + : Guid.NewGuid()); + var quarantineRoot = Path.Combine( + transactionsRoot, + QuarantineDirectoryName); + try + { + Directory.CreateDirectory(quarantineRoot); + MachineIntegrationTransactionFileSystem.EnsureDirectoryIsNotReparsePoint(quarantineRoot); + var quarantineDirectory = Path.Combine( + quarantineRoot, + $"{transactionId:D}.{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}.{Guid.NewGuid():N}"); + Directory.Move(stagingDirectory, quarantineDirectory); + + var declaredBytes = handoff is null + ? 0 + : MachineIntegrationTransactionFileSystem.GetDeclaredArtifactBytes(handoff.Context.Artifacts); + var manifest = new MachineIntegrationQuarantineManifest( + transactionId, + reason, + exception?.GetType().FullName, + exception?.Message, + DateTimeOffset.UtcNow, + handoff?.Context.Artifacts.Count ?? 0, + declaredBytes, + materializedBytes); + TryWriteQuarantineManifest(quarantineDirectory, manifest); + TryReportProgress( + progress, + transactionId, + MachineIntegrationTransferPhase.Quarantined, + null, + materializedBytes, + declaredBytes, + 0, + manifest.ArtifactCount); + return true; + } + catch + { + // Keep the original publish or cleanup failure and leave staging in place for diagnosis. + return false; + } + } + + private static void TryWriteQuarantineManifest( + string quarantineDirectory, + MachineIntegrationQuarantineManifest manifest) + { + try + { + MachineIntegrationTransactionFileSystem.WriteMessage( + Path.Combine(quarantineDirectory, QuarantineManifestFileName), + JsonSerializer.SerializeToUtf8Bytes(manifest)); + } + catch + { + // The quarantined bytes remain the primary evidence if the manifest cannot be written. + } + } + + private static void TryReportProgress( + IProgress? progress, + Guid transactionId, + MachineIntegrationTransferPhase phase, + string? artifactId, + long bytesCopied, + long totalBytes, + int completedArtifacts, + int artifactCount) + { + try + { + progress?.Report(new( + transactionId, + phase, + artifactId, + bytesCopied, + totalBytes, + completedArtifacts, + artifactCount)); + } + catch + { + // A completion observer cannot turn an already-published transaction into a failure. + } + } +} diff --git a/src/OpenVisionLab.Machine.Persistence/OpenVisionLab.Machine.Persistence.csproj b/src/OpenVisionLab.Machine.Persistence/OpenVisionLab.Machine.Persistence.csproj new file mode 100644 index 0000000..f677354 --- /dev/null +++ b/src/OpenVisionLab.Machine.Persistence/OpenVisionLab.Machine.Persistence.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + OpenVisionLab.Machine.Persistence + OpenVisionLab.Machine.Persistence + + + + + + + diff --git a/src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileCatalog.cs b/src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileCatalog.cs new file mode 100644 index 0000000..0ce1806 --- /dev/null +++ b/src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileCatalog.cs @@ -0,0 +1,40 @@ +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.Machine.Persistence.Projects; + +public sealed record ProjectDocumentFileEntry( + string FullPath, + string FileName, + MachineProjectDocument Document); + +/// +/// Enumerates and loads project documents from a directory. Presentation +/// models should consume these entries instead of performing file I/O. +/// +public sealed class ProjectDocumentFileCatalog +{ + private readonly ProjectDocumentFileStore _fileStore; + + public ProjectDocumentFileCatalog(ProjectDocumentFileStore? fileStore = null) + { + _fileStore = fileStore ?? new ProjectDocumentFileStore(); + } + + public IEnumerable Enumerate( + string directory, + string searchPattern = "*.ovmachine") + { + ArgumentException.ThrowIfNullOrWhiteSpace(directory); + ArgumentException.ThrowIfNullOrWhiteSpace(searchPattern); + + foreach (var fullPath in Directory + .EnumerateFiles(directory, searchPattern) + .OrderBy(path => Path.GetFileName(path), StringComparer.OrdinalIgnoreCase)) + { + yield return new( + fullPath, + Path.GetFileName(fullPath), + _fileStore.Load(fullPath)); + } + } +} diff --git a/src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileStore.cs b/src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileStore.cs new file mode 100644 index 0000000..96c28bc --- /dev/null +++ b/src/OpenVisionLab.Machine.Persistence/Projects/ProjectDocumentFileStore.cs @@ -0,0 +1,152 @@ +using System.Runtime.ExceptionServices; +using System.Text.Json; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.Machine.Persistence.Projects; + +/// +/// Owns file-system persistence for a machine project document. +/// JSON serialization and schema normalization stay with the Core document store; +/// this adapter adds paths, atomic replacement, backup recovery, and cancellation. +/// +public sealed class ProjectDocumentFileStore +{ + private readonly ProjectDocumentStore _documentStore; + + public ProjectDocumentFileStore(ProjectDocumentStore? documentStore = null) + { + _documentStore = documentStore ?? new ProjectDocumentStore(); + } + + public async Task SaveAsync( + MachineProjectDocument document, + string path, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var fullPath = Path.GetFullPath(path); + var directory = Path.GetDirectoryName(fullPath) + ?? throw new ArgumentException("The project path must include a directory.", nameof(path)); + var temporaryPath = Path.Combine( + directory, + $".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp"); + var modifiedAt = DateTimeOffset.UtcNow; + var json = _documentStore.SerializeForSave(document, modifiedAt); + + try + { + await File.WriteAllTextAsync(temporaryPath, json, cancellationToken).ConfigureAwait(false); + if (File.Exists(fullPath)) + { + File.Replace(temporaryPath, fullPath, fullPath + ".bak", ignoreMetadataErrors: true); + } + else + { + File.Move(temporaryPath, fullPath); + } + + document.Schema = MachineProjectDocument.CurrentSchema; + document.ModifiedAt = modifiedAt; + } + finally + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } + } + } + + public async Task LoadAsync( + string path, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var fullPath = Path.GetFullPath(path); + try + { + return await LoadFileAsync(fullPath, cancellationToken).ConfigureAwait(false); + } + catch (Exception primaryException) when (ShouldTryBackup(primaryException)) + { + try + { + return await LoadFileAsync(fullPath + ".bak", cancellationToken).ConfigureAwait(false); + } + catch (Exception backupException) when (IsExpectedLoadFailure(backupException)) + { + if (backupException is ProjectDocumentLoadException + { + ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema + }) + { + ExceptionDispatchInfo.Capture(backupException).Throw(); + } + + ExceptionDispatchInfo.Capture(primaryException).Throw(); + throw; + } + } + } + + public MachineProjectDocument Load(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var fullPath = Path.GetFullPath(path); + try + { + return LoadFile(fullPath); + } + catch (Exception primaryException) when (ShouldTryBackup(primaryException)) + { + try + { + return LoadFile(fullPath + ".bak"); + } + catch (Exception backupException) when (IsExpectedLoadFailure(backupException)) + { + if (backupException is ProjectDocumentLoadException + { + ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema + }) + { + ExceptionDispatchInfo.Capture(backupException).Throw(); + } + + ExceptionDispatchInfo.Capture(primaryException).Throw(); + throw; + } + } + } + + private async Task LoadFileAsync( + string path, + CancellationToken cancellationToken) + { + var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + return _documentStore.Load(json); + } + + private MachineProjectDocument LoadFile(string path) => + _documentStore.Load(File.ReadAllText(path)); + + private static bool ShouldTryBackup(Exception exception) => exception switch + { + ProjectDocumentLoadException + { + ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema + } => false, + _ => IsExpectedLoadFailure(exception) + }; + + private static bool IsExpectedLoadFailure(Exception exception) => exception switch + { + ProjectDocumentLoadException => true, + JsonException => true, + IOException => true, + UnauthorizedAccessException => true, + ArgumentOutOfRangeException => true, + _ => false + }; +} diff --git a/src/OpenVisionLab.Machine.Sequence/Runtime/DeterministicSequenceExecutor.cs b/src/OpenVisionLab.Machine.Sequence/Runtime/DeterministicSequenceExecutor.cs index 8494431..166d5a9 100644 --- a/src/OpenVisionLab.Machine.Sequence/Runtime/DeterministicSequenceExecutor.cs +++ b/src/OpenVisionLab.Machine.Sequence/Runtime/DeterministicSequenceExecutor.cs @@ -451,7 +451,9 @@ private SequenceExecutionResult RouteOrFault(CompiledSequenceStep step, Sequence private SequenceExecutionResult Fault(SequenceExecutionError error) { _lastError = error; - if (_frames.Count > 1) + // A watchdog exhausts the execution budget, including nested calls. + // Only ordinary step failures may recover through a caller's error route. + if (error.Code != SequenceExecutionErrorCode.SequenceWatchdogTimedOut && _frames.Count > 1) { var child = CurrentFrame!; var parent = _frames[^2]; diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/ConfigureAxesCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/ConfigureAxesCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/ConfigureAxesCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/ConfigureAxesCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/HomeAxisCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/HomeAxisCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/HomeAxisCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/HomeAxisCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/JogAxisCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/JogAxisCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/JogAxisCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/JogAxisCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/MoveAbsoluteCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/MoveAbsoluteCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/MoveAbsoluteCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/MoveAbsoluteCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/MoveRelativeCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/MoveRelativeCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/MoveRelativeCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/MoveRelativeCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/MoveVelocityCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/MoveVelocityCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/MoveVelocityCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/MoveVelocityCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StopAxesCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/StopAxesCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StopAxesCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/StopAxesCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StopAxisCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Axis/StopAxisCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StopAxisCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Axis/StopAxisCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/TriggerVirtualCameraCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Camera/TriggerVirtualCameraCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/TriggerVirtualCameraCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Camera/TriggerVirtualCameraCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SetConveyorCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetConveyorCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SetConveyorCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetConveyorCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SetCylinderCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetCylinderCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SetCylinderCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetCylinderCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SetDigitalSensorForceCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetDigitalSensorForceCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SetDigitalSensorForceCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetDigitalSensorForceCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SetVirtualInputCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetVirtualInputCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SetVirtualInputCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetVirtualInputCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SetVirtualInputForceCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetVirtualInputForceCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SetVirtualInputForceCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Equipment/SetVirtualInputForceCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/ClearSimulationFaultCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Faults/ClearSimulationFaultCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/ClearSimulationFaultCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Faults/ClearSimulationFaultCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/InjectSimulationFaultCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Faults/InjectSimulationFaultCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/InjectSimulationFaultCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Faults/InjectSimulationFaultCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/ConfigureRuntimeCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/ConfigureRuntimeCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/ConfigureRuntimeCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/ConfigureRuntimeCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/MoveAxesAbsoluteCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/MoveAxesAbsoluteCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/MoveAxesAbsoluteCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/MoveAxesAbsoluteCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/PauseCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/PauseCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/PauseCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/PauseCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/PlayCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/PlayCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/PlayCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/PlayCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/ResetCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/ResetCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/ResetCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/ResetCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SimulationCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/SimulationCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SimulationCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/SimulationCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SimulationCommandResult.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/SimulationCommandResult.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SimulationCommandResult.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/SimulationCommandResult.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StartAutomaticRunCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StartAutomaticRunCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StartAutomaticRunCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StartAutomaticRunCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StartConditionScenarioCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StartConditionScenarioCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StartConditionScenarioCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StartConditionScenarioCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StartManualControlCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StartManualControlCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StartManualControlCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StartManualControlCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StepCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StepCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StepCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StepCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StopConditionScenarioCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StopConditionScenarioCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StopConditionScenarioCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/RunControl/StopConditionScenarioCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/AbortSequenceCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Sequence/AbortSequenceCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/AbortSequenceCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Sequence/AbortSequenceCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/RetrySequenceCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Sequence/RetrySequenceCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/RetrySequenceCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Sequence/RetrySequenceCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/SetSequenceBreakpointCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Sequence/SetSequenceBreakpointCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/SetSequenceBreakpointCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Sequence/SetSequenceBreakpointCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StartSequenceCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Sequence/StartSequenceCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StartSequenceCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Sequence/StartSequenceCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Commands/StepSequenceCommand.cs b/src/OpenVisionLab.Machine.Simulation/Commands/Sequence/StepSequenceCommand.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Commands/StepSequenceCommand.cs rename to src/OpenVisionLab.Machine.Simulation/Commands/Sequence/StepSequenceCommand.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRunConfiguration.cs b/src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/AutomaticRunConfiguration.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRunConfiguration.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/AutomaticRunConfiguration.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationAutomaticRunCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/SimulationAutomaticRunCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationAutomaticRunCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/SimulationAutomaticRunCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationAutomaticRunCycleHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/SimulationAutomaticRunCycleHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationAutomaticRunCycleHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/SimulationAutomaticRunCycleHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationAutomaticRunRuntime.cs b/src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/SimulationAutomaticRunRuntime.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationAutomaticRunRuntime.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/AutomaticRun/SimulationAutomaticRunRuntime.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioProgressHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioProgressHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioProgressHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioProgressHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioRuntime.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioRuntime.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioRuntime.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioRuntime.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioStopHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioStopHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScenarioStopHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ConditionScenario/SimulationConditionScenarioStopHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScheduledFaultInjectionHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationConditionScheduledFaultInjectionHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScheduledFaultInjectionHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationConditionScheduledFaultInjectionHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScheduledFaultRecoveryHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationConditionScheduledFaultRecoveryHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationConditionScheduledFaultRecoveryHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationConditionScheduledFaultRecoveryHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationEngineFaultPoint.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationEngineFaultPoint.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationEngineFaultPoint.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationEngineFaultPoint.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationFaultCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationFaultCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationFaultCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationFaultCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationFaultRuntime.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationFaultRuntime.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationFaultRuntime.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Faults/SimulationFaultRuntime.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualAxisCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualAxisCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualAxisCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualAxisCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualCameraCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualCameraCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualCameraCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualCameraCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualControlCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualControlCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualControlCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualControlCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualEquipmentCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualEquipmentCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualEquipmentCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualEquipmentCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualInputCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualInputCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationManualInputCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationManualInputCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationPhysicalRuntimeTick.cs b/src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationPhysicalRuntimeTick.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationPhysicalRuntimeTick.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/ManualControl/SimulationPhysicalRuntimeTick.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/DeterministicSimulationCommandTraceStore.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/DeterministicSimulationCommandTraceStore.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/DeterministicSimulationCommandTraceStore.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/DeterministicSimulationCommandTraceStore.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/FixedStepSimulationEngine.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/FixedStepSimulationEngine.cs similarity index 99% rename from src/OpenVisionLab.Machine.Simulation/Engine/FixedStepSimulationEngine.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/FixedStepSimulationEngine.cs index 01b34ed..2d2f70f 100644 --- a/src/OpenVisionLab.Machine.Simulation/Engine/FixedStepSimulationEngine.cs +++ b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/FixedStepSimulationEngine.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Globalization; using System.Threading.Channels; using OpenVisionLab.Machine.IO.Channels; using OpenVisionLab.Machine.Sequence.Runtime; @@ -195,6 +196,9 @@ public Task EnqueueCommandAsync( private async Task RunLoop(CancellationToken cancellationToken) { + // Canonical events include numeric messages. Keep this worker's async + // context deterministic without changing the caller's UI culture. + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var timing = new SimulationRunLoopTiming(_settings.FixedStep, _settings.MaxCatchUpTicks); timing.Reset(stopwatch.Elapsed); diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/ISimulationEngine.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/ISimulationEngine.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/ISimulationEngine.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/ISimulationEngine.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationClock.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationClock.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationClock.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationClock.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationControlOwner.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationControlOwner.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationControlOwner.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationControlOwner.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationEngineLifecycle.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationEngineLifecycle.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationEngineLifecycle.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationEngineLifecycle.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationEngineTermination.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationEngineTermination.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationEngineTermination.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationEngineTermination.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationRunControlCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRunControlCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationRunControlCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRunControlCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationRunLoopTiming.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRunLoopTiming.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationRunLoopTiming.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRunLoopTiming.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationRunMode.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRunMode.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationRunMode.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRunMode.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationRuntimeConfiguration.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRuntimeConfiguration.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationRuntimeConfiguration.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRuntimeConfiguration.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationRuntimeConfigurationBuilder.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRuntimeConfigurationBuilder.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationRuntimeConfigurationBuilder.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRuntimeConfigurationBuilder.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationRuntimeState.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRuntimeState.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationRuntimeState.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationRuntimeState.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationSettings.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationSettings.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationSettings.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Runtime/SimulationSettings.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/DeterministicSequenceRuntimeContext.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Sequence/DeterministicSequenceRuntimeContext.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/DeterministicSequenceRuntimeContext.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Sequence/DeterministicSequenceRuntimeContext.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationSequenceCommandHandler.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Sequence/SimulationSequenceCommandHandler.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationSequenceCommandHandler.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Sequence/SimulationSequenceCommandHandler.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Engine/SimulationSequenceRuntime.cs b/src/OpenVisionLab.Machine.Simulation/Engine/Sequence/SimulationSequenceRuntime.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Engine/SimulationSequenceRuntime.cs rename to src/OpenVisionLab.Machine.Simulation/Engine/Sequence/SimulationSequenceRuntime.cs diff --git a/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioHeadlessRunner.cs b/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioHeadlessRunner.cs index 1cc4770..285df31 100644 --- a/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioHeadlessRunner.cs +++ b/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioHeadlessRunner.cs @@ -7,6 +7,7 @@ using OpenVisionLab.Machine.Simulation.Commands; using OpenVisionLab.Machine.Simulation.Compilation; using OpenVisionLab.Machine.Simulation.Engine; +using OpenVisionLab.Machine.Simulation.Scenarios; namespace OpenVisionLab.Machine.Simulation.FaultScenarios; @@ -197,7 +198,10 @@ public async Task WriteReportAsync( } var payload = JsonSerializer.Serialize(report, DeterministicFaultScenarioHeadlessRunReport.ReportJsonOptions); - await File.WriteAllTextAsync(fullPath, payload, cancellationToken).ConfigureAwait(false); + await AtomicEvidenceFile.WriteAsync( + fullPath, + (temporaryPath, token) => File.WriteAllTextAsync(temporaryPath, payload, token), + cancellationToken).ConfigureAwait(false); } private static string[] RuntimeCompilationErrors( diff --git a/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioProfile.cs b/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioProfile.cs index 580dbdb..e1b2ed0 100644 --- a/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioProfile.cs +++ b/src/OpenVisionLab.Machine.Simulation/FaultScenarios/DeterministicFaultScenarioProfile.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using OpenVisionLab.Machine.Simulation.Scenarios; namespace OpenVisionLab.Machine.Simulation.FaultScenarios; @@ -74,8 +75,11 @@ public sealed record DeterministicFaultScenarioProfile( public static string SaveToJson(DeterministicFaultScenarioProfile scenario) => JsonSerializer.Serialize(Normalize(scenario), JsonOptions); - public static void SaveToJson(DeterministicFaultScenarioProfile scenario, string path) => - File.WriteAllText(path, SaveToJson(scenario)); + public static void SaveToJson(DeterministicFaultScenarioProfile scenario, string path) + { + var fullPath = Path.GetFullPath(path); + AtomicEvidenceFile.Write(fullPath, temporaryPath => File.WriteAllText(temporaryPath, SaveToJson(scenario))); + } public static DeterministicFaultScenarioProfile Normalize(DeterministicFaultScenarioProfile? scenario) { diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicScenarioAssertionEvaluator.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Assertions/DeterministicScenarioAssertionEvaluator.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicScenarioAssertionEvaluator.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Assertions/DeterministicScenarioAssertionEvaluator.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicScenarioAssertions.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Assertions/DeterministicScenarioAssertions.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicScenarioAssertions.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Assertions/DeterministicScenarioAssertions.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationBatchResultComparer.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Batch/DeterministicSimulationBatchResultComparer.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationBatchResultComparer.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Batch/DeterministicSimulationBatchResultComparer.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationBatchResultPackage.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Batch/DeterministicSimulationBatchResultPackage.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationBatchResultPackage.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Batch/DeterministicSimulationBatchResultPackage.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationBatchRunner.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Batch/DeterministicSimulationBatchRunner.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationBatchRunner.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Batch/DeterministicSimulationBatchRunner.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationCommandTrace.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/CommandTrace/DeterministicSimulationCommandTrace.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationCommandTrace.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/CommandTrace/DeterministicSimulationCommandTrace.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationCommandTraceCommandCodec.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/CommandTrace/DeterministicSimulationCommandTraceCommandCodec.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationCommandTraceCommandCodec.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/CommandTrace/DeterministicSimulationCommandTraceCommandCodec.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationCommandTraceReplayRunner.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/CommandTrace/DeterministicSimulationCommandTraceReplayRunner.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationCommandTraceReplayRunner.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/CommandTrace/DeterministicSimulationCommandTraceReplayRunner.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicUnifiedCommissioningEvidencePackage.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Commissioning/DeterministicUnifiedCommissioningEvidencePackage.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicUnifiedCommissioningEvidencePackage.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Commissioning/DeterministicUnifiedCommissioningEvidencePackage.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioProfile.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioProfile.cs similarity index 97% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioProfile.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioProfile.cs index 73182ed..b4362ff 100644 --- a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioProfile.cs +++ b/src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioProfile.cs @@ -71,8 +71,11 @@ public sealed record DeterministicConditionScenarioProfile( public static string SaveToJson(DeterministicConditionScenarioProfile profile) => JsonSerializer.Serialize(Normalize(profile), JsonOptions); - public static void SaveToJson(DeterministicConditionScenarioProfile profile, string path) => - File.WriteAllText(path, SaveToJson(profile)); + public static void SaveToJson(DeterministicConditionScenarioProfile profile, string path) + { + var fullPath = Path.GetFullPath(path); + AtomicEvidenceFile.Write(fullPath, temporaryPath => File.WriteAllText(temporaryPath, SaveToJson(profile))); + } public static DeterministicConditionScenarioProfile Normalize( DeterministicConditionScenarioProfile? profile) diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioReplayResult.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioReplayResult.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioReplayResult.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioReplayResult.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioRunner.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioRunner.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioRunner.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioRunner.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioRuntime.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioRuntime.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicConditionScenarioRuntime.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Condition/DeterministicConditionScenarioRuntime.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/AtomicEvidenceFile.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/AtomicEvidenceFile.cs similarity index 54% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/AtomicEvidenceFile.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/AtomicEvidenceFile.cs index ef4e90a..cc8cf06 100644 --- a/src/OpenVisionLab.Machine.Simulation/Scenarios/AtomicEvidenceFile.cs +++ b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/AtomicEvidenceFile.cs @@ -1,7 +1,7 @@ namespace OpenVisionLab.Machine.Simulation.Scenarios; /// -/// Owns temporary-file lifetime and replacement for Simulation evidence. +/// Owns temporary-file lifetime and replacement for Simulation JSON artifacts. /// Callers retain validation, path preparation, serialization, and encoding. /// internal static class AtomicEvidenceFile @@ -23,4 +23,25 @@ internal static void Write(string fullPath, Action writeTemporaryFile) } } } + + internal static async Task WriteAsync( + string fullPath, + Func writeTemporaryFile, + CancellationToken cancellationToken = default) + { + var temporaryPath = $"{fullPath}.{Guid.NewGuid():N}.tmp"; + try + { + await writeTemporaryFile(temporaryPath, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + File.Move(temporaryPath, fullPath, overwrite: true); + } + finally + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } + } + } } diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationEvidenceExchangePackage.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationEvidenceExchangePackage.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationEvidenceExchangePackage.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationEvidenceExchangePackage.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationRunEvidenceHasher.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationRunEvidenceHasher.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationRunEvidenceHasher.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationRunEvidenceHasher.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationRunResultComparer.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationRunResultComparer.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationRunResultComparer.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationRunResultComparer.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationRunResultPackage.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationRunResultPackage.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicSimulationRunResultPackage.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/DeterministicSimulationRunResultPackage.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/SimulationEvidenceRetentionPolicy.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/SimulationEvidenceRetentionPolicy.cs similarity index 100% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/SimulationEvidenceRetentionPolicy.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Evidence/SimulationEvidenceRetentionPolicy.cs diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicVisionExecutionEvidencePackage.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionEvidencePackage.cs similarity index 81% rename from src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicVisionExecutionEvidencePackage.cs rename to src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionEvidencePackage.cs index 7745dc5..f63b9d4 100644 --- a/src/OpenVisionLab.Machine.Simulation/Scenarios/DeterministicVisionExecutionEvidencePackage.cs +++ b/src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionEvidencePackage.cs @@ -474,116 +474,3 @@ private static string HashEvidence( private static string Hash(string value) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))); } - -public sealed class DeterministicVisionExecutionRecorder -{ - private readonly string _projectId; - private readonly string _projectName; - private readonly string _projectPath; - private readonly string _projectJson; - private readonly string _buildIdentity; - private readonly TimeSpan _fixedStep; - private readonly long _triggerTick; - private readonly string _commandId; - private readonly string _cameraId; - private readonly string _recipeId; - private readonly string _acquisitionId; - private readonly string _frameId; - private readonly string _inspectionId; - private readonly List _events = []; - private bool _started; - - public DeterministicVisionExecutionRecorder( - string projectId, - string projectName, - string projectPath, - string projectJson, - string buildIdentity, - TimeSpan fixedStep, - long triggerTick, - string commandId, - string cameraId, - string recipeId, - string acquisitionId, - string frameId, - string inspectionId) - { - _projectId = projectId; - _projectName = projectName; - _projectPath = projectPath; - _projectJson = projectJson; - _buildIdentity = buildIdentity; - _fixedStep = fixedStep; - _triggerTick = triggerTick; - _commandId = commandId; - _cameraId = cameraId; - _recipeId = recipeId; - _acquisitionId = acquisitionId; - _frameId = frameId; - _inspectionId = inspectionId; - } - - public bool IsReady { get; private set; } - - public void RecordEvent(SimulationEvent runtimeEvent) - { - ArgumentNullException.ThrowIfNull(runtimeEvent); - if (!_started) - { - if (!string.Equals(runtimeEvent.Code, "CameraTriggered", StringComparison.Ordinal) - || !string.Equals(runtimeEvent.CommandId, _commandId, StringComparison.Ordinal)) - { - return; - } - _started = true; - } - - if (IsReady) - { - return; - } - - _events.Add(runtimeEvent); - if (string.Equals(runtimeEvent.Code, "VisionResultReady", StringComparison.Ordinal) - && runtimeEvent.Message.Contains(_acquisitionId, StringComparison.Ordinal) - && runtimeEvent.Message.Contains(_inspectionId, StringComparison.Ordinal)) - { - IsReady = true; - } - } - - public bool CanComplete(SimulationSnapshot snapshot) - { - var camera = snapshot.Cameras.FirstOrDefault(item => - string.Equals(item.Id, _cameraId, StringComparison.Ordinal)); - var result = camera?.Result; - return IsReady - && camera?.State == VirtualCameraState.FrameReady - && string.Equals(result?.AcquisitionId, _acquisitionId, StringComparison.Ordinal) - && string.Equals(result?.RecipeId, _recipeId, StringComparison.Ordinal) - && string.Equals(result?.FrameEvidence?.FrameId ?? camera.FrameEvidence?.FrameId, _frameId, StringComparison.Ordinal) - && string.Equals(result?.InspectionEvidence?.InspectionId, _inspectionId, StringComparison.Ordinal); - } - - public DeterministicVisionExecutionEvidencePackage Complete(SimulationSnapshot snapshot) - { - if (!CanComplete(snapshot)) - { - throw new InvalidOperationException("Vision execution evidence is not complete."); - } - - var camera = snapshot.Cameras.Single(item => - string.Equals(item.Id, _cameraId, StringComparison.Ordinal)); - return DeterministicVisionExecutionEvidencePackage.Create( - _projectId, - _projectName, - _projectPath, - _projectJson, - _buildIdentity, - _fixedStep, - _triggerTick, - snapshot, - camera, - _events); - } -} diff --git a/src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionRecorder.cs b/src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionRecorder.cs new file mode 100644 index 0000000..df7b757 --- /dev/null +++ b/src/OpenVisionLab.Machine.Simulation/Scenarios/Vision/DeterministicVisionExecutionRecorder.cs @@ -0,0 +1,122 @@ +using OpenVisionLab.Machine.Simulation.Camera; +using OpenVisionLab.Machine.Simulation.Events; +using OpenVisionLab.Machine.Simulation.Snapshots; + +namespace OpenVisionLab.Machine.Simulation.Scenarios; + +/// +/// Correlates one manual camera transaction with runtime events and completes +/// it into the immutable Vision evidence package. +/// +public sealed class DeterministicVisionExecutionRecorder +{ + private readonly string _projectId; + private readonly string _projectName; + private readonly string _projectPath; + private readonly string _projectJson; + private readonly string _buildIdentity; + private readonly TimeSpan _fixedStep; + private readonly long _triggerTick; + private readonly string _commandId; + private readonly string _cameraId; + private readonly string _recipeId; + private readonly string _acquisitionId; + private readonly string _frameId; + private readonly string _inspectionId; + private readonly List _events = []; + private bool _started; + + public DeterministicVisionExecutionRecorder( + string projectId, + string projectName, + string projectPath, + string projectJson, + string buildIdentity, + TimeSpan fixedStep, + long triggerTick, + string commandId, + string cameraId, + string recipeId, + string acquisitionId, + string frameId, + string inspectionId) + { + _projectId = projectId; + _projectName = projectName; + _projectPath = projectPath; + _projectJson = projectJson; + _buildIdentity = buildIdentity; + _fixedStep = fixedStep; + _triggerTick = triggerTick; + _commandId = commandId; + _cameraId = cameraId; + _recipeId = recipeId; + _acquisitionId = acquisitionId; + _frameId = frameId; + _inspectionId = inspectionId; + } + + public bool IsReady { get; private set; } + + public void RecordEvent(SimulationEvent runtimeEvent) + { + ArgumentNullException.ThrowIfNull(runtimeEvent); + if (!_started) + { + if (!string.Equals(runtimeEvent.Code, "CameraTriggered", StringComparison.Ordinal) + || !string.Equals(runtimeEvent.CommandId, _commandId, StringComparison.Ordinal)) + { + return; + } + _started = true; + } + + if (IsReady) + { + return; + } + + _events.Add(runtimeEvent); + if (string.Equals(runtimeEvent.Code, "VisionResultReady", StringComparison.Ordinal) + && runtimeEvent.Message.Contains(_acquisitionId, StringComparison.Ordinal) + && runtimeEvent.Message.Contains(_inspectionId, StringComparison.Ordinal)) + { + IsReady = true; + } + } + + public bool CanComplete(SimulationSnapshot snapshot) + { + var camera = snapshot.Cameras.FirstOrDefault(item => + string.Equals(item.Id, _cameraId, StringComparison.Ordinal)); + var result = camera?.Result; + return IsReady + && camera?.State == VirtualCameraState.FrameReady + && string.Equals(result?.AcquisitionId, _acquisitionId, StringComparison.Ordinal) + && string.Equals(result?.RecipeId, _recipeId, StringComparison.Ordinal) + && string.Equals(result?.FrameEvidence?.FrameId ?? camera.FrameEvidence?.FrameId, _frameId, StringComparison.Ordinal) + && string.Equals(result?.InspectionEvidence?.InspectionId, _inspectionId, StringComparison.Ordinal); + } + + public DeterministicVisionExecutionEvidencePackage Complete(SimulationSnapshot snapshot) + { + if (!CanComplete(snapshot)) + { + throw new InvalidOperationException("Vision execution evidence is not complete."); + } + + var camera = snapshot.Cameras.Single(item => + string.Equals(item.Id, _cameraId, StringComparison.Ordinal)); + return DeterministicVisionExecutionEvidencePackage.Create( + _projectId, + _projectName, + _projectPath, + _projectJson, + _buildIdentity, + _fixedStep, + _triggerTick, + snapshot, + camera, + _events); + } +} diff --git a/src/OpenVisionLab.MachineStudio/OpenVisionLab.MachineStudio.csproj b/src/OpenVisionLab.MachineStudio/OpenVisionLab.MachineStudio.csproj index d011473..5f0e425 100644 --- a/src/OpenVisionLab.MachineStudio/OpenVisionLab.MachineStudio.csproj +++ b/src/OpenVisionLab.MachineStudio/OpenVisionLab.MachineStudio.csproj @@ -31,6 +31,7 @@ + diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeCameraCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Camera/SmokeCameraCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeCameraCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Camera/SmokeCameraCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeCameraFirstUseVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Camera/SmokeCameraFirstUseVerifier.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeCameraFirstUseVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Camera/SmokeCameraFirstUseVerifier.cs index 19fe141..238e000 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeCameraFirstUseVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Camera/SmokeCameraFirstUseVerifier.cs @@ -9,6 +9,7 @@ using OpenVisionLab; using OpenVisionLab.Machine.Core.Devices; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.Machine.Core.Sequences; using OpenVisionLab.Machine.Simulation.Camera; using OpenVisionLab.Machine.Simulation.Compilation; @@ -178,6 +179,7 @@ step.NextStepId is null var normalBorderThickness = buttonBorder.BorderThickness; var blankProject = CurrentProject(viewModel); var store = new ProjectDocumentStore(); + var fileStore = new ProjectDocumentFileStore(store); var blankEvidence = store.SerializeForEvidence(blankProject); var snapshotBefore = viewModel.SceneSnapshots.Latest ?? throw new InvalidOperationException("The initial runtime snapshot was unavailable."); @@ -460,7 +462,7 @@ step.NextStepId is null Directory.CreateDirectory(Path.GetDirectoryName(fullSavePath)!); await viewModel.SaveProjectAsync(fullSavePath); Check("first-use-project-saved", File.Exists(fullSavePath) && !viewModel.HasUnsavedChanges); - var savedProject = await store.LoadAsync(fullSavePath); + var savedProject = await fileStore.LoadAsync(fullSavePath); Check("saved-state-exact", HasExactInspectionGraph(savedProject) && authoredEvidence == store.SerializeForEvidence(savedProject)); Check("saved-project-compiles", new MachineProjectRuntimeCompiler( diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeAnalogIoAuthoringVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAnalogIoAuthoringVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeAnalogIoAuthoringVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAnalogIoAuthoringVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeAxisCommissioningStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAxisCommissioningStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeAxisCommissioningStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAxisCommissioningStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeAxisCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAxisCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeAxisCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAxisCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeAxisTuningStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAxisTuningStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeAxisTuningStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeAxisTuningStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeConveyorCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeConveyorCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeConveyorCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeConveyorCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeCylinderCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeCylinderCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeCylinderCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeCylinderCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeDigitalIoCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeDigitalIoCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeDigitalIoCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeDigitalIoCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLoadLockSetupVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeLoadLockSetupVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeLoadLockSetupVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeLoadLockSetupVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeMultiAxisCommissioningStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeMultiAxisCommissioningStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeMultiAxisCommissioningStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeMultiAxisCommissioningStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeMultiAxisCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeMultiAxisCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeMultiAxisCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeMultiAxisCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeSensorCommissioningVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeSensorCommissioningVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeSensorCommissioningVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Commissioning/SmokeSensorCommissioningVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeConnectionWorkbenchWorkflow.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeConnectionWorkbenchWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeConnectionWorkbenchWorkflow.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeConnectionWorkbenchWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeFaultScenarioHost.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeFaultScenarioHost.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeFaultScenarioHost.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeFaultScenarioHost.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeArgumentParser.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeArgumentParser.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeArgumentParser.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeArgumentParser.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeFailurePolicy.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeFailurePolicy.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeFailurePolicy.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeFailurePolicy.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeHost.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeHost.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeHost.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeHost.cs index 7249f81..2140cf8 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeHost.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeHost.cs @@ -122,6 +122,14 @@ public static async Task RunAsync(string[] args) var windowCapture = new SmokeWindowCapture(); var uiInteraction = CreateUiInteraction(window, windowCapture, nativeInput); + if (GetArgumentValue(args, "--smoke-exit-report") is { } exitReportPath) + { + Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var exitReport = await SmokeProjectSafetyVerifier.VerifyExitAsync(window, vm, exitReportPath, windowCapture, nativeInput); + Application.Current.Shutdown(exitReport.IsValid ? 0 : 1); + return; + } + if (!string.IsNullOrWhiteSpace(smokeOptions.AnalogIoAuthoringState)) { var analogAuthoringReport = await SmokeAnalogIoAuthoringVerifier.VerifyAsync( diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeOptions.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeOptions.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeOptions.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeOptions.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeProjectLoader.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeProjectLoader.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/DirectExeSmokeProjectLoader.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/DirectExeSmokeProjectLoader.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/MachineIntegrationExeSmoke.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/MachineIntegrationExeSmoke.cs similarity index 96% rename from src/OpenVisionLab.MachineStudio/Smoke/MachineIntegrationExeSmoke.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/MachineIntegrationExeSmoke.cs index 324136e..9c6771e 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/MachineIntegrationExeSmoke.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Host/MachineIntegrationExeSmoke.cs @@ -138,8 +138,8 @@ void Check(string name, bool passed) peerPort, sharedKey, mode); - Check("machineSettingsCanBeSaved", viewModel.Integration.SaveSetupCommand.CanExecute(null)); - viewModel.Integration.SaveSetupCommand.Execute(null); + Check("machineSettingsCanBeSaved", viewModel.Integration.Setup.SaveSetupCommand.CanExecute(null)); + viewModel.Integration.Setup.SaveSetupCommand.Execute(null); await Task.Delay(100); Check("machineSettingsSaved", File.Exists(settingsPath)); @@ -177,9 +177,9 @@ void Check(string name, bool passed) && string.Equals(step.TargetId, viewModel.SelectedCameraId, StringComparison.Ordinal) && string.Equals(step.Parameter, viewModel.SelectedCameraRecipe, StringComparison.Ordinal))); Check("integrationNotBusy", !viewModel.Integration.IsBusy); - Check("consumerIdentityAvailable", viewModel.Integration.TwoDConsumerIdentity is not null); - Check("exchangeRootAvailable", Directory.Exists(viewModel.Integration.ExchangeRoot.Trim())); - Check("integrationRecipeAvailable", File.Exists(viewModel.Integration.InspectionRecipePath.Trim())); + Check("consumerIdentityAvailable", viewModel.Integration.Setup.TwoDConsumerIdentity is not null); + Check("exchangeRootAvailable", Directory.Exists(viewModel.Integration.Setup.ExchangeRoot.Trim())); + Check("integrationRecipeAvailable", File.Exists(viewModel.Integration.Setup.InspectionRecipePath.Trim())); Check( "cameraFrameSourceMatches", camera?.FrameEvidence is { } frame @@ -403,20 +403,20 @@ private static void ConfigureMachineIntegration( byte[] sharedKey, string mode) { - integration.ExchangeRoot = machineRoot; - integration.InspectionRecipePath = recipePath; - integration.TwoDConsumerVersion = + integration.Setup.ExchangeRoot = machineRoot; + integration.Setup.InspectionRecipePath = recipePath; + integration.Setup.TwoDConsumerVersion = string.Equals(mode, "2d", StringComparison.OrdinalIgnoreCase) ? consumerVersion : string.Empty; - integration.TwoDConsumerCommit = + integration.Setup.TwoDConsumerCommit = string.Equals(mode, "2d", StringComparison.OrdinalIgnoreCase) ? consumerCommit : string.Empty; - integration.TcpListenAddress = "127.0.0.1"; - integration.TcpListenPortText = listenPort.ToString(); - integration.TcpPeerHost = "127.0.0.1"; - integration.TcpPeerPortText = peerPort.ToString(); + integration.Setup.TcpListenAddress = "127.0.0.1"; + integration.Setup.TcpListenPortText = listenPort.ToString(); + integration.Setup.TcpPeerHost = "127.0.0.1"; + integration.Setup.TcpPeerPortText = peerPort.ToString(); integration.SetSessionSharedKey(Convert.ToBase64String(sharedKey)); } diff --git a/src/OpenVisionLab.MachineStudio/Smoke/MachineIntegrationExeSmokeCameraScenario.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/MachineIntegrationExeSmokeCameraScenario.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/MachineIntegrationExeSmokeCameraScenario.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/MachineIntegrationExeSmokeCameraScenario.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokePerformanceVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Host/SmokePerformanceVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokePerformanceVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Host/SmokePerformanceVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeConnectionWorkbenchVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Integration/SmokeConnectionWorkbenchVerifier.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeConnectionWorkbenchVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Integration/SmokeConnectionWorkbenchVerifier.cs index fec141b..83bae24 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeConnectionWorkbenchVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Integration/SmokeConnectionWorkbenchVerifier.cs @@ -9,6 +9,7 @@ using OpenVisionLab.Machine.Core.Channels; using OpenVisionLab.Machine.Core.Layouts; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.Machine.Sequence.Authoring; using OpenVisionLab.Machine.Simulation.Compilation; using OpenVisionLab.Machine.Simulation.Layout; @@ -349,7 +350,7 @@ stageRow is not null await vm.SaveProjectAsync(fullSavePath); Check("saved-project-reopens", await vm.OpenProjectAsync(fullSavePath)); - var saved = await new ProjectDocumentStore().LoadAsync(fullSavePath); + var saved = await new ProjectDocumentFileStore().LoadAsync(fullSavePath); var savedLayout = saved.Layouts.First(layout => string.Equals(layout.Id, saved.Simulation.ActiveLayoutId, StringComparison.Ordinal)); var savedStage = savedLayout.Components.Single(component => component.Id == stageId); diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeIntegrationResultVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Integration/SmokeIntegrationResultVerifier.cs similarity index 98% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeIntegrationResultVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Integration/SmokeIntegrationResultVerifier.cs index 11a2993..1434dbc 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeIntegrationResultVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Integration/SmokeIntegrationResultVerifier.cs @@ -81,7 +81,7 @@ void Check(string name, bool passed) $"The integration exchange root was not found: {fullExchangeRoot}"); } - viewModel.Integration.ExchangeRoot = fullExchangeRoot; + viewModel.Integration.Setup.ExchangeRoot = fullExchangeRoot; } if (state is "result" or "rejected") @@ -131,7 +131,7 @@ void Check(string name, bool passed) Check("window-intersects-selected-monitor", monitor.WindowIntersectsMonitor); Check("exchange-root-observed", string.IsNullOrWhiteSpace(exchangeRoot) || string.Equals( - viewModel.Integration.ExchangeRoot, + viewModel.Integration.Setup.ExchangeRoot, Path.GetFullPath(exchangeRoot), StringComparison.OrdinalIgnoreCase)); Check("tcp-listen-address-rendered", @@ -219,7 +219,7 @@ void Check(string name, bool passed) return new SmokeIntegrationResultReport { State = state, - ExchangeRoot = viewModel.Integration.ExchangeRoot, + ExchangeRoot = viewModel.Integration.Setup.ExchangeRoot, AcknowledgementStatusText = viewModel.Integration.AcknowledgementStatusText, ResultStatusText = viewModel.Integration.ResultStatusText, StatusText = viewModel.Integration.StatusText, diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeCanvasNavigationVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeCanvasNavigationVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeCanvasNavigationVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeCanvasNavigationVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeDirectSceneAuthoringVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeDirectSceneAuthoringVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeDirectSceneAuthoringVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeDirectSceneAuthoringVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeDirectTransformVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeDirectTransformVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeDirectTransformVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeDirectTransformVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeEditMenuStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeEditMenuStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeEditMenuStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeEditMenuStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeEvidenceDrawerStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeEvidenceDrawerStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeEvidenceDrawerStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeEvidenceDrawerStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeGlobalCommandStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeGlobalCommandStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeGlobalCommandStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeGlobalCommandStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLayerOrderVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayerOrderVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeLayerOrderVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayerOrderVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutAlignmentVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutAlignmentVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutAlignmentVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutAlignmentVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutHistoryVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutHistoryVerifier.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutHistoryVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutHistoryVerifier.cs index 8dad0b9..d10d241 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutHistoryVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutHistoryVerifier.cs @@ -3,6 +3,7 @@ using System.Windows.Input; using OpenVisionLab.Machine.Core.Layouts; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.ViewModel; using static OpenVisionLab.MachineStudio.SmokeRoundTripScenario; @@ -192,7 +193,7 @@ static void Execute(ICommand command, object? parameter = null) Directory.CreateDirectory(reportDirectory); var projectPath = Path.Combine(reportDirectory, "layout-history-roundtrip.ovmachine"); await viewModel.SaveProjectAsync(projectPath); - var savedProject = await new ProjectDocumentStore().LoadAsync(projectPath); + var savedProject = await new ProjectDocumentFileStore().LoadAsync(projectPath); var validation = new MachineProjectLayoutValidator().Validate(savedProject); Check("pastedProjectValid", validation.IsValid); var projectStore = new ProjectDocumentStore(); diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutPropertyStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutPropertyStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeLayoutPropertyStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLayoutPropertyStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeLibraryDropVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLibraryDropVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeLibraryDropVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeLibraryDropVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeMultiSelectionTransformVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeMultiSelectionTransformVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeMultiSelectionTransformVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Layout/SmokeMultiSelectionTransformVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectRoundTripVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectRoundTripVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectRoundTripVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectRoundTripVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectSafetyVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectSafetyVerifier.cs similarity index 76% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectSafetyVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectSafetyVerifier.cs index 1b73c7b..ed8be94 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectSafetyVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectSafetyVerifier.cs @@ -46,6 +46,132 @@ internal static class SmokeProjectSafetyVerifier private const byte VirtualKeyReturn = 0x0D; private const byte VirtualKeyEscape = 0x1B; + internal static async Task VerifyExitAsync( + ShellWindow window, MainViewModel vm, string reportPath, SmokeWindowCapture capture, SmokeNativeInput input) + { + var checks = new Dictionary(StringComparer.Ordinal); + var monitor = SmokeDpiTestHook.CaptureMonitorEvidence(window); + var closed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var approvedAttempt = false; + var promptCount = 0; + SmokeProjectSafetyReport Report() => new() + { + Checks = checks, + Failures = checks.Where(check => !check.Value).Select(check => check.Key).ToArray(), + Monitor = monitor + }; + void OnClosed(object? sender, EventArgs args) + { + checks["closed-only-after-approved-attempt"] = approvedAttempt; + var closeTask = vm.RequestCloseAsync(); + checks["session-completed-before-window-closed"] = closeTask.IsCompletedSuccessfully && closeTask.Result.IsApproved; + checks["resources-disposed-before-window-closed"] = closeTask.IsCompletedSuccessfully + && closeTask.Result.Shutdown is { Outcome: RuntimeShutdownOutcome.Completed, Stage: "ResourceDispose" }; + // Persist here too: the broken Application.Shutdown path can destroy + // the dispatcher before the awaiting smoke continuation runs. + Report().Save(reportPath); + closed.TrySetResult(); + } + + window.Closed += OnClosed; + try + { + checks["window-on-selected-monitor"] = monitor.WindowIntersectsMonitor; + checks["dirty-edit-created"] = vm.TryAddLayoutComponent(LayoutComponentKind.MachineFrame) && vm.HasUnsavedChanges; + vm.UnsavedProjectPrompt = () => + { + promptCount++; + return UnsavedProjectDecision.Cancel; + }; + var menu = SmokeVisualTreeQuery.FindVisualDescendant(window)!; + var fileMenu = (MenuItem)menu.Items[0]; + var exitItem = fileMenu.Items.OfType().Single(item => ReferenceEquals(item.Command, vm.ExitCommand)); + input.ActivateWindow(window); + for (var attempt = 0; attempt < 20 && !window.IsActive; attempt++) + { + await Task.Delay(25); + input.ActivateWindow(window); + } + checks["window-owns-keyboard-input"] = window.IsActive; + fileMenu.IsSubmenuOpen = true; + await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); + capture.SetPopupContent((fileMenu.Template.FindName("PART_Popup", fileMenu) as System.Windows.Controls.Primitives.Popup)?.Child as FrameworkElement); + checks["rendered-exit-binding-enabled"] = exitItem.IsVisible && exitItem.IsEnabled && exitItem.Command.CanExecute(null); + capture.Capture(window, reportPath + ".before-exit.png"); + exitItem.Focus(); + input.MovePointerToCenter(exitItem); + await Task.Delay(50); + await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); + checks["exit-hover-hit-region"] = exitItem.IsMouseOver; + input.PressLeftButton(); + input.MarkPointerHeld(); + await Task.Delay(50); + await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); + checks["actual-pointer-down-observed"] = Mouse.LeftButton == MouseButtonState.Pressed; + capture.Capture(window, reportPath + ".pointer-down.png"); + // Deliver mouse-up while the menu still owns capture; cleanup may + // release capture only after WPF has processed the native input. + input.SendMouseEvent(0x0004, 0, 0, 0, UIntPtr.Zero); + for (var attempt = 0; attempt < 200 && (promptCount < 1 || !vm.ExitCommand.CanExecute(null)); attempt++) + { + await Task.Delay(10); + } + input.ReleasePointer(); + await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); + checks["cancel-keeps-window-and-dirty-project"] = !closed.Task.IsCompleted && window.IsVisible && vm.HasUnsavedChanges; + checks[$"cancel-prompt-shown-once-observed-{promptCount}"] = promptCount == 1; + capture.SetPopupContent(null); + capture.Capture(window, reportPath + ".cancelled-exit.png"); + + input.ActivateWindow(window); + fileMenu.Focus(); + await Task.Delay(50); + input.SendKey(0x28); // Down opens the File menu through WPF keyboard handling. + await Task.Delay(50); + input.SendKey(0x26); // Up wraps from the first item to Exit. + await Task.Delay(50); + await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); + checks["keyboard-menu-focus-visible"] = fileMenu.IsKeyboardFocusWithin && exitItem.IsHighlighted; + capture.SetPopupContent((fileMenu.Template.FindName("PART_Popup", fileMenu) as System.Windows.Controls.Primitives.Popup)?.Child as FrameworkElement); + capture.Capture(window, reportPath + ".keyboard-focus.png"); + input.SendKey(VirtualKeyReturn); + for (var attempt = 0; attempt < 200 && (promptCount < 2 || !vm.ExitCommand.CanExecute(null)); attempt++) + { + await Task.Delay(10); + } + await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); + checks[$"keyboard-exit-cancelled-observed-{promptCount}"] = promptCount == 2 && !closed.Task.IsCompleted && window.IsVisible; + + vm.RunCommand.Execute(null); + var deadline = System.Diagnostics.Stopwatch.StartNew(); + while (!vm.IsRunning && deadline.Elapsed < TimeSpan.FromSeconds(5)) + { + await Task.Delay(10); + } + checks["runtime-active-before-approved-exit"] = vm.IsRunning; + var promptsBeforeApproval = promptCount; + vm.UnsavedProjectPrompt = () => + { + promptCount++; + return UnsavedProjectDecision.Discard; + }; + approvedAttempt = true; + vm.ExitCommand.Execute(null); + vm.ExitCommand.Execute(null); + await closed.Task.WaitAsync(TimeSpan.FromSeconds(10)); + checks["duplicate-exit-shares-one-close-request"] = promptCount == promptsBeforeApproval + 1; + var report = Report(); + report.Save(reportPath); + return report; + } + finally + { + input.ReleasePointer(); + capture.SetPopupContent(null); + window.Closed -= OnClosed; + } + } + public static async Task VerifyAsync( ShellWindow window, MainViewModel vm, diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectTreeQuery.cs b/src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectTreeQuery.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProjectTreeQuery.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Project/SmokeProjectTreeQuery.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeBatchPersistenceVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeBatchPersistenceVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeBatchPersistenceVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeBatchPersistenceVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokePickAndPlaceStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokePickAndPlaceStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokePickAndPlaceStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokePickAndPlaceStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockApplicationVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockApplicationVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockApplicationVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockApplicationVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockEditVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockEditVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockEditVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockEditVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockPreparation.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockPreparation.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockPreparation.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockPreparation.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockProposalVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockProposalVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockProposalVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockProposalVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockSequenceStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockSequenceStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockSequenceStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockSequenceStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockStepStatusVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockStepStatusVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockStepStatusVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockStepStatusVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockTimeoutVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockTimeoutVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeProcessBlockTimeoutVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeProcessBlockTimeoutVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeCheckpointStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeCheckpointStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeCheckpointStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeCheckpointStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeConnectionStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeConnectionStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeConnectionStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeConnectionStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeDryRunStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeDryRunStateVerifier.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeDryRunStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeDryRunStateVerifier.cs index 93567b9..bd0efca 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeDryRunStateVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeDryRunStateVerifier.cs @@ -11,6 +11,7 @@ using OpenVisionLab.Machine.Core.Axes; using OpenVisionLab.Machine.Core.Channels; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.Machine.Core.Sequences; using OpenVisionLab.Machine.Core.Layouts; using OpenVisionLab.Machine.Core.Devices; @@ -219,7 +220,7 @@ public static async Task ApplyAsync( AssertSmoke( await vm.OpenProjectAsync(savePath), "The saved wafer-handler recipe could not be reopened."); - var reopened = await new ProjectDocumentStore().LoadAsync(savePath); + var reopened = await new ProjectDocumentFileStore().LoadAsync(savePath); var reopenedHandler = reopened.Devices.Single(device => device.Kind == DeviceKind.Handler && device.WaferHandler is not null); AssertSmoke( @@ -240,7 +241,7 @@ await vm.OpenProjectAsync(savePath), AssertSmoke( await vm.OpenProjectAsync(savePath), "The saved inspection-sorter recipe could not be reopened."); - var reopened = await new ProjectDocumentStore().LoadAsync(savePath); + var reopened = await new ProjectDocumentFileStore().LoadAsync(savePath); var reopenedSorter = reopened.Devices.Single(device => device.Kind == DeviceKind.Sorter && device.InspectionSortRouter is not null); AssertSmoke( @@ -261,7 +262,7 @@ await vm.OpenProjectAsync(savePath), AssertSmoke( await vm.OpenProjectAsync(savePath), "The saved inspection-handoff recipe could not be reopened."); - var reopened = await new ProjectDocumentStore().LoadAsync(savePath); + var reopened = await new ProjectDocumentFileStore().LoadAsync(savePath); var reopenedHandoff = reopened.Devices.Single(device => device.Kind == DeviceKind.Inspection && device.InspectionHandoff is not null); AssertSmoke( @@ -285,7 +286,7 @@ await vm.OpenProjectAsync(savePath), AssertSmoke( await vm.OpenProjectAsync(savePath), "The saved OHT handoff recipe could not be reopened."); - var reopened = await new ProjectDocumentStore().LoadAsync(savePath); + var reopened = await new ProjectDocumentFileStore().LoadAsync(savePath); var reopenedHandoff = reopened.Devices.Single(device => device.Kind == DeviceKind.Oht && device.OhtHandoff is not null); AssertSmoke( @@ -307,7 +308,7 @@ await vm.OpenProjectAsync(savePath), AssertSmoke( await vm.OpenProjectAsync(savePath), "The saved pre-aligner recipe could not be reopened."); - var reopened = await new ProjectDocumentStore().LoadAsync(savePath); + var reopened = await new ProjectDocumentFileStore().LoadAsync(savePath); var reopenedPrealigner = reopened.Devices.Single(device => device.Kind == DeviceKind.Prealigner && device.Prealigner is not null); AssertSmoke( diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeGalleryVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeGalleryVerifier.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeGalleryVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeGalleryVerifier.cs index 368b0aa..d9d8be4 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRecipeGalleryVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeRecipeGalleryVerifier.cs @@ -6,6 +6,7 @@ using System.Windows.Threading; using OpenVisionLab; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.View.Shell; using OpenVisionLab.MachineStudio.ViewModel; @@ -391,7 +392,7 @@ initialProject is null ?? throw new InvalidOperationException("Recipe gallery selection was not available."); var fullCopyPath = Path.GetFullPath(recipeGalleryCopyPath); Directory.CreateDirectory(Path.GetDirectoryName(fullCopyPath)!); - var store = new ProjectDocumentStore(); + var store = new ProjectDocumentFileStore(); var sourceProject = await store.LoadAsync(selectedRecipe.SourcePath); var created = await vm.SemiconductorRecipes.CreateCopyToAsync(fullCopyPath); var copiedProject = File.Exists(fullCopyPath) diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeSemanticSetupVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeSemanticSetupVerifier.cs similarity index 64% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeSemanticSetupVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeSemanticSetupVerifier.cs index 1d08a28..8806d6b 100644 --- a/src/OpenVisionLab.MachineStudio/Smoke/SmokeSemanticSetupVerifier.cs +++ b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeSemanticSetupVerifier.cs @@ -53,6 +53,7 @@ void Check(bool condition, string message) var semanticStore = new ProjectDocumentStore(); var semanticBefore = semanticStore.Serialize(project); var semanticRuntimeBefore = vm.SceneSnapshots.Latest; + var semantic = vm.RecipeConnections.SemanticSetups; var semanticKind = project.Devices.Any(device => device.Kind == DeviceKind.Prealigner) ? "prealigner" : project.Devices.Any(device => device.Kind == DeviceKind.Handler) ? "wafer-handler" : project.Devices.Any(device => device.Kind == DeviceKind.Inspection) ? "inspection-handoff" @@ -61,33 +62,33 @@ void Check(bool condition, string message) switch (semanticKind) { case "prealigner": - vm.RecipeConnections.SemanticSetups.PreviewPrealignerSetupCommand.Execute(null); + semantic.Prealigner.PreviewCommand.Execute(null); break; case "wafer-handler": - vm.RecipeConnections.SemanticSetups.PreviewWaferHandlerSetupCommand.Execute(null); + semantic.WaferHandler.PreviewCommand.Execute(null); break; case "inspection-handoff": - vm.RecipeConnections.SemanticSetups.PreviewInspectionHandoffSetupCommand.Execute(null); + semantic.InspectionHandoff.PreviewCommand.Execute(null); break; case "inspection-sort": - vm.RecipeConnections.SemanticSetups.PreviewInspectionSortRouterSetupCommand.Execute(null); + semantic.InspectionSortRouter.PreviewCommand.Execute(null); break; default: - vm.RecipeConnections.SemanticSetups.PreviewOhtHandoffSetupCommand.Execute(null); + semantic.OhtHandoff.PreviewCommand.Execute(null); break; } await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); Check( (semanticKind == "prealigner" - ? vm.RecipeConnections.SemanticSetups.IsPrealignerSetupVisible + ? semantic.Prealigner.IsVisible : semanticKind == "wafer-handler" - ? vm.RecipeConnections.SemanticSetups.IsWaferHandlerSetupVisible + ? semantic.WaferHandler.IsVisible : semanticKind == "inspection-handoff" - ? vm.RecipeConnections.SemanticSetups.IsInspectionHandoffSetupVisible + ? semantic.InspectionHandoff.IsVisible : semanticKind == "inspection-sort" - ? vm.RecipeConnections.SemanticSetups.IsInspectionSortRouterSetupVisible - : vm.RecipeConnections.SemanticSetups.IsOhtHandoffSetupVisible) + ? semantic.InspectionSortRouter.IsVisible + : semantic.OhtHandoff.IsVisible) && semanticBefore == semanticStore.Serialize(project) && semanticRuntimeBefore?.TickIndex == vm.SceneSnapshots.Latest?.TickIndex && !vm.IsRunning @@ -100,13 +101,13 @@ void Check(bool condition, string message) switch (semanticKind) { case "inspection-handoff": - vm.RecipeConnections.SemanticSetups.InspectionHandoffCameraId = missingId; + semantic.InspectionHandoff.CameraId = missingId; break; case "inspection-sort": - vm.RecipeConnections.SemanticSetups.InspectionSortCameraId = missingId; + semantic.InspectionSortRouter.CameraId = missingId; break; case "oht": - vm.RecipeConnections.SemanticSetups.OhtTransportConveyorId = missingId; + semantic.OhtHandoff.TransportConveyorId = missingId; break; default: throw new InvalidOperationException( @@ -121,15 +122,15 @@ void Check(bool condition, string message) await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); var preserved = semanticKind switch { - "inspection-handoff" => vm.RecipeConnections.SemanticSetups.InspectionHandoffCameraId == missingId - && vm.RecipeConnections.SemanticSetups.HasInspectionHandoffSetupValidationError - && vm.RecipeConnections.SemanticSetups.InspectionCameraOptions.Any(option => option.Id == missingId), - "inspection-sort" => vm.RecipeConnections.SemanticSetups.InspectionSortCameraId == missingId - && vm.RecipeConnections.SemanticSetups.HasInspectionSortRouterSetupValidationError - && vm.RecipeConnections.SemanticSetups.InspectionCameraOptions.Any(option => option.Id == missingId), - _ => vm.RecipeConnections.SemanticSetups.OhtTransportConveyorId == missingId - && vm.RecipeConnections.SemanticSetups.HasOhtHandoffSetupValidationError - && vm.RecipeConnections.SemanticSetups.InspectionConveyorOptions.Any(option => option.Id == missingId) + "inspection-handoff" => semantic.InspectionHandoff.CameraId == missingId + && semantic.InspectionHandoff.HasValidationError + && semantic.InspectionSortRouter.CameraOptions.Any(option => option.Id == missingId), + "inspection-sort" => semantic.InspectionSortRouter.CameraId == missingId + && semantic.InspectionSortRouter.HasValidationError + && semantic.InspectionHandoff.CameraOptions.Any(option => option.Id == missingId), + _ => semantic.OhtHandoff.TransportConveyorId == missingId + && semantic.OhtHandoff.HasValidationError + && semantic.OhtHandoff.ConveyorOptions.Any(option => option.Id == missingId) }; Check( preserved @@ -160,37 +161,37 @@ void Check(bool condition, string message) switch (semanticKind) { case "prealigner": - vm.RecipeConnections.SemanticSetups.PrealignerRotaryStageComponentId = - vm.RecipeConnections.SemanticSetups.PrealignerClampCylinderComponentId; + semantic.Prealigner.RotaryStageComponentId = + semantic.Prealigner.ClampCylinderComponentId; break; case "wafer-handler": - vm.RecipeConnections.SemanticSetups.WaferHandlerHorizontalAxisId = - vm.RecipeConnections.SemanticSetups.WaferHandlerVerticalAxisId; + semantic.WaferHandler.HorizontalAxisId = + semantic.WaferHandler.VerticalAxisId; break; case "inspection-handoff": - vm.RecipeConnections.SemanticSetups.InspectionHandoffCameraId = null; + semantic.InspectionHandoff.CameraId = null; break; case "inspection-sort": - vm.RecipeConnections.SemanticSetups.InspectionSortNgConveyorId = - vm.RecipeConnections.SemanticSetups.InspectionSortPassConveyorId; + semantic.InspectionSortRouter.NgConveyorId = + semantic.InspectionSortRouter.PassConveyorId; break; default: - vm.RecipeConnections.SemanticSetups.OhtVehicleDockedChannelId = - vm.RecipeConnections.SemanticSetups.OhtRouteAvailableChannelId; + semantic.OhtHandoff.VehicleDockedChannelId = + semantic.OhtHandoff.RouteAvailableChannelId; break; } await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); Check( semanticKind == "prealigner" - ? vm.RecipeConnections.SemanticSetups.HasPrealignerSetupValidationError + ? semantic.Prealigner.HasValidationError : semanticKind == "wafer-handler" - ? vm.RecipeConnections.SemanticSetups.HasWaferHandlerSetupValidationError + ? semantic.WaferHandler.HasValidationError : semanticKind == "inspection-handoff" - ? vm.RecipeConnections.SemanticSetups.HasInspectionHandoffSetupValidationError + ? semantic.InspectionHandoff.HasValidationError : semanticKind == "inspection-sort" - ? vm.RecipeConnections.SemanticSetups.HasInspectionSortRouterSetupValidationError - : vm.RecipeConnections.SemanticSetups.HasOhtHandoffSetupValidationError, + ? semantic.InspectionSortRouter.HasValidationError + : semantic.OhtHandoff.HasValidationError, "Invalid semantic setup did not block Apply."); return; } @@ -217,29 +218,29 @@ void Check(bool condition, string message) switch (semanticKind) { case "prealigner": - vm.RecipeConnections.SemanticSetups.ApplyPrealignerSetupCommand.Execute(null); + semantic.Prealigner.ApplyCommand.Execute(null); break; case "wafer-handler": - vm.RecipeConnections.SemanticSetups.ApplyWaferHandlerSetupCommand.Execute(null); + semantic.WaferHandler.ApplyCommand.Execute(null); break; case "inspection-handoff": - vm.RecipeConnections.SemanticSetups.ApplyInspectionHandoffSetupCommand.Execute(null); + semantic.InspectionHandoff.ApplyCommand.Execute(null); break; case "inspection-sort": - vm.RecipeConnections.SemanticSetups.ApplyInspectionSortRouterSetupCommand.Execute(null); + semantic.InspectionSortRouter.ApplyCommand.Execute(null); break; default: - vm.RecipeConnections.SemanticSetups.ApplyOhtHandoffSetupCommand.Execute(null); + semantic.OhtHandoff.ApplyCommand.Execute(null); break; } await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); Check( - !vm.RecipeConnections.SemanticSetups.IsPrealignerSetupVisible - && !vm.RecipeConnections.SemanticSetups.IsWaferHandlerSetupVisible - && !vm.RecipeConnections.SemanticSetups.IsInspectionHandoffSetupVisible - && !vm.RecipeConnections.SemanticSetups.IsInspectionSortRouterSetupVisible - && !vm.RecipeConnections.SemanticSetups.IsOhtHandoffSetupVisible + !semantic.Prealigner.IsVisible + && !semantic.WaferHandler.IsVisible + && !semantic.InspectionHandoff.IsVisible + && !semantic.InspectionSortRouter.IsVisible + && !semantic.OhtHandoff.IsVisible && !vm.IsRunning && vm.IsDesignMode && semanticRuntimeBefore?.TickIndex == vm.SceneSnapshots.Latest?.TickIndex, @@ -269,33 +270,33 @@ await vm.OpenProjectAsync(savePath), switch (semanticKind) { case "prealigner": - vm.RecipeConnections.SemanticSetups.PreviewPrealignerSetupCommand.Execute(null); + semantic.Prealigner.PreviewCommand.Execute(null); break; case "wafer-handler": - vm.RecipeConnections.SemanticSetups.PreviewWaferHandlerSetupCommand.Execute(null); + semantic.WaferHandler.PreviewCommand.Execute(null); break; case "inspection-handoff": - vm.RecipeConnections.SemanticSetups.PreviewInspectionHandoffSetupCommand.Execute(null); + semantic.InspectionHandoff.PreviewCommand.Execute(null); break; case "inspection-sort": - vm.RecipeConnections.SemanticSetups.PreviewInspectionSortRouterSetupCommand.Execute(null); + semantic.InspectionSortRouter.PreviewCommand.Execute(null); break; default: - vm.RecipeConnections.SemanticSetups.PreviewOhtHandoffSetupCommand.Execute(null); + semantic.OhtHandoff.PreviewCommand.Execute(null); break; } await window.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); Check( (semanticKind == "prealigner" - ? vm.RecipeConnections.SemanticSetups.IsPrealignerSetupVisible + ? semantic.Prealigner.IsVisible : semanticKind == "wafer-handler" - ? vm.RecipeConnections.SemanticSetups.IsWaferHandlerSetupVisible + ? semantic.WaferHandler.IsVisible : semanticKind == "inspection-handoff" - ? vm.RecipeConnections.SemanticSetups.IsInspectionHandoffSetupVisible + ? semantic.InspectionHandoff.IsVisible : semanticKind == "inspection-sort" - ? vm.RecipeConnections.SemanticSetups.IsInspectionSortRouterSetupVisible - : vm.RecipeConnections.SemanticSetups.IsOhtHandoffSetupVisible) + ? semantic.InspectionSortRouter.IsVisible + : semantic.OhtHandoff.IsVisible) && !vm.IsRunning && vm.IsDesignMode, "Saved semantic setup was not restored safely after reopen."); diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeStationSkeletonVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeStationSkeletonVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeStationSkeletonVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Recipes/SmokeStationSkeletonVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeFaultManagerVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeFaultManagerVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeFaultManagerVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeFaultManagerVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRuntimeDebuggerVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeRuntimeDebuggerVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRuntimeDebuggerVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeRuntimeDebuggerVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRuntimeEvidenceVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeRuntimeEvidenceVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRuntimeEvidenceVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeRuntimeEvidenceVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeScenarioBatchVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeScenarioBatchVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeScenarioBatchVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeScenarioBatchVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeSequenceStateVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeSequenceStateVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeSequenceStateVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeSequenceStateVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeTestScenarioRuntimeVerifier.cs b/src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeTestScenarioRuntimeVerifier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeTestScenarioRuntimeVerifier.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Runtime/SmokeTestScenarioRuntimeVerifier.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeButtonPointerState.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeButtonPointerState.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeButtonPointerState.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeButtonPointerState.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeNativeInput.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeNativeInput.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeNativeInput.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeNativeInput.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeRoundTripScenario.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeRoundTripScenario.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeRoundTripScenario.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeRoundTripScenario.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeUiInteraction.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeUiInteraction.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeUiInteraction.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeUiInteraction.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeVisualTreeQuery.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeVisualTreeQuery.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeVisualTreeQuery.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeVisualTreeQuery.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeWindowCapture.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeWindowCapture.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeWindowCapture.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeWindowCapture.cs diff --git a/src/OpenVisionLab.MachineStudio/Smoke/SmokeWorkflowReport.cs b/src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeWorkflowReport.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/Smoke/SmokeWorkflowReport.cs rename to src/OpenVisionLab.MachineStudio/Smoke/Support/SmokeWorkflowReport.cs diff --git a/src/OpenVisionLab.MachineStudio/View/Inspector/RightToolRegionView.xaml b/src/OpenVisionLab.MachineStudio/View/Inspector/RightToolRegionView.xaml index c945886..3c121c0 100644 --- a/src/OpenVisionLab.MachineStudio/View/Inspector/RightToolRegionView.xaml +++ b/src/OpenVisionLab.MachineStudio/View/Inspector/RightToolRegionView.xaml @@ -2000,15 +2000,15 @@ - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - + @@ -893,20 +893,20 @@ BorderBrush="{DynamicResource Border.Subtle}" BorderThickness="1" CornerRadius="6" - Visibility="{Binding RecipeConnections.SemanticSetups.IsPrealignerSetupVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> + Visibility="{Binding RecipeConnections.SemanticSetups.Prealigner.IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> - + - - - - - - - - + + + + + + + + - + @@ -918,17 +918,17 @@ BorderBrush="{DynamicResource Border.Subtle}" BorderThickness="1" CornerRadius="6" - Visibility="{Binding RecipeConnections.SemanticSetups.IsInspectionHandoffSetupVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> + Visibility="{Binding RecipeConnections.SemanticSetups.InspectionHandoff.IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> - + - - - - - + + + + + - + @@ -940,17 +940,17 @@ BorderBrush="{DynamicResource Border.Subtle}" BorderThickness="1" CornerRadius="6" - Visibility="{Binding RecipeConnections.SemanticSetups.IsInspectionSortRouterSetupVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> + Visibility="{Binding RecipeConnections.SemanticSetups.InspectionSortRouter.IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> - + - - - - - + + + + + - + @@ -962,19 +962,19 @@ BorderBrush="{DynamicResource Border.Subtle}" BorderThickness="1" CornerRadius="6" - Visibility="{Binding RecipeConnections.SemanticSetups.IsOhtHandoffSetupVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> + Visibility="{Binding RecipeConnections.SemanticSetups.OhtHandoff.IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> - + - - - - - - - + + + + + + + - + diff --git a/src/OpenVisionLab.MachineStudio/View/Shell/MainWpfInteractionHost.cs b/src/OpenVisionLab.MachineStudio/View/Shell/MainWpfInteractionHost.cs index ccc0d9f..d2036d2 100644 --- a/src/OpenVisionLab.MachineStudio/View/Shell/MainWpfInteractionHost.cs +++ b/src/OpenVisionLab.MachineStudio/View/Shell/MainWpfInteractionHost.cs @@ -6,12 +6,22 @@ namespace OpenVisionLab.MachineStudio.View.Shell; internal sealed class MainWpfInteractionHost { - internal void ShutdownApplication() => Application.Current.Shutdown(); + private readonly Dispatcher? _dispatcher; + + internal MainWpfInteractionHost(Dispatcher? dispatcher = null) + { + _dispatcher = dispatcher; + } + + // ShellWindow waits for save decisions and runtime shutdown before closing. + internal void RequestApplicationClose() => Application.Current.MainWindow?.Close(); internal async Task CommitFocusedEditorAsync() { Keyboard.ClearFocus(); - if (Application.Current?.Dispatcher is { } dispatcher) + if (GetDispatcher() is { } dispatcher + && !dispatcher.HasShutdownStarted + && !dispatcher.HasShutdownFinished) { await dispatcher.InvokeAsync( () => { }, @@ -21,8 +31,14 @@ await dispatcher.InvokeAsync( internal async Task DispatchAsync(Action action) { - var dispatcher = Application.Current?.Dispatcher; - if (dispatcher is null || dispatcher.HasShutdownStarted) + var dispatcher = GetDispatcher(); + if (dispatcher is null) + { + action(); + return; + } + + if (dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished) { return; } @@ -32,8 +48,19 @@ internal async Task DispatchAsync(Action action) internal async Task DispatchBatchProgressAsync(Action action) { - var dispatcher = Application.Current?.Dispatcher; - if (dispatcher is null || dispatcher.HasShutdownStarted || dispatcher.CheckAccess()) + var dispatcher = GetDispatcher(); + if (dispatcher is null) + { + action(); + return; + } + + if (dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished) + { + return; + } + + if (dispatcher.CheckAccess()) { action(); return; @@ -45,9 +72,21 @@ internal async Task DispatchBatchProgressAsync(Action action) internal Task DispatchOnUiThreadAsync(Func operation) { ArgumentNullException.ThrowIfNull(operation); - var dispatcher = Application.Current?.Dispatcher; - return dispatcher is null || dispatcher.HasShutdownStarted || dispatcher.CheckAccess() + var dispatcher = GetDispatcher(); + if (dispatcher is null) + { + return operation(); + } + + if (dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished) + { + return Task.CompletedTask; + } + + return dispatcher.CheckAccess() ? operation() : dispatcher.InvokeAsync(operation).Task.Unwrap(); } + + private Dispatcher? GetDispatcher() => _dispatcher ?? Application.Current?.Dispatcher; } diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Commissioning/MultiAxisCommissioningViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Commissioning/MultiAxisCommissioningViewModel.cs index 5317f2b..30efeb3 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Commissioning/MultiAxisCommissioningViewModel.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Commissioning/MultiAxisCommissioningViewModel.cs @@ -328,19 +328,25 @@ private async Task ValidateAsync( } catch (Exception exception) when (exception is InvalidDataException or ArgumentException) { - _setStatus(OpenVisionLanguageService.T("Axis.RecipeValidationRejected")); - _log($"Commissioning validation rejected · {exception.Message}"); + await DispatchPresentationAsync(() => + { + _setStatus(OpenVisionLanguageService.T("Axis.RecipeValidationRejected")); + _log($"Commissioning validation rejected · {exception.Message}"); + }); return new(MultiAxisCommissioningParticipantOutcome.Failed, Exception: exception); } var projectJson = _serializeProject(); var projectPath = _getProjectPath() ?? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, $"unsaved-{project.Id}.ovmachine")); - _artifactStore.SetLatestResult(null); - _completedRuns = 0; - SetValidationRunning(true); - _setStatus(OpenVisionLanguageService.T("Axis.RecipeValidationStarted")); - _log($"Commissioning repeat validation started · {recipe.ValidationRepetitions} run(s)"); + await DispatchPresentationAsync(() => + { + _artifactStore.SetLatestResult(null); + _completedRuns = 0; + SetValidationRunning(true); + _setStatus(OpenVisionLanguageService.T("Axis.RecipeValidationStarted")); + _log($"Commissioning repeat validation started · {recipe.ValidationRepetitions} run(s)"); + }); try { var result = await new DeterministicMultiAxisCommissioningRunner().RunAsync( @@ -353,32 +359,46 @@ private async Task ValidateAsync( _fixedStep, UpdateProgressAsync, cancellationToken); - _artifactStore.SetLatestResult(result); - _artifactStore.AppendHistory(result, DateTimeOffset.UtcNow); - SelectedHistoryEntry = ResultHistory.Entries[^1]; - _baselineComparison = AcceptedBaseline?.CompareTo(result); - _setStatus(result.IsSuccess - ? OpenVisionLanguageService.T("Axis.RecipeValidationPassedStatus") - : OpenVisionLanguageService.T("Axis.RecipeValidationMismatchStatus")); - _log( - result.IsSuccess - ? $"Commissioning repeat validation passed · {result.CompletedRuns} run(s) · {ShortHash(result.EvidenceHash)}" - : $"Commissioning repeat validation mismatch · run {result.FirstMismatch?.RunIndex} · {result.FirstMismatch?.EvidenceKind} · Tick {result.FirstMismatch?.TickIndex}"); - PersistResult(); + await DispatchPresentationAsync(() => + { + _artifactStore.SetLatestResult(result); + _artifactStore.AppendHistory(result, DateTimeOffset.UtcNow); + SelectedHistoryEntry = ResultHistory.Entries[^1]; + _baselineComparison = AcceptedBaseline?.CompareTo(result); + _setStatus(result.IsSuccess + ? OpenVisionLanguageService.T("Axis.RecipeValidationPassedStatus") + : OpenVisionLanguageService.T("Axis.RecipeValidationMismatchStatus")); + _log( + result.IsSuccess + ? $"Commissioning repeat validation passed · {result.CompletedRuns} run(s) · {ShortHash(result.EvidenceHash)}" + : $"Commissioning repeat validation mismatch · run {result.FirstMismatch?.RunIndex} · {result.FirstMismatch?.EvidenceKind} · Tick {result.FirstMismatch?.TickIndex}"); + PersistResult(); + }); return new(MultiAxisCommissioningParticipantOutcome.Completed, result); } catch (Exception exception) when (exception is InvalidOperationException or ArgumentException) { - _setStatus(OpenVisionLanguageService.T("Axis.RecipeValidationRejected")); - _log($"Commissioning repeat validation rejected · {exception.Message}"); + await DispatchPresentationAsync(() => + { + _setStatus(OpenVisionLanguageService.T("Axis.RecipeValidationRejected")); + _log($"Commissioning repeat validation rejected · {exception.Message}"); + }); return new(MultiAxisCommissioningParticipantOutcome.Failed, Exception: exception); } finally { - SetValidationRunning(false); + await DispatchPresentationAsync(() => SetValidationRunning(false)); } } + private Task DispatchPresentationAsync(Action action) => _dispatchToUi(() => + { + if (!_disposed) + { + action(); + } + }); + private Task UpdateProgressAsync(int completedRuns) => _dispatchToUi(() => { _completedRuns = completedRuns; diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationRequestWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationRequestWorkflow.cs index a897d2e..445e0a5 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationRequestWorkflow.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationRequestWorkflow.cs @@ -105,7 +105,11 @@ internal static string CreateRefreshKey(MachineIntegrationRequestContext context sourceDefinition.Width, sourceDefinition.Height, inspectionRecipePath, - frame, + new MachineIntegrationFrameEvidence( + frame.FrameId, + frame.SourceRelativePath, + frame.ContentSha256, + frame.ContentLength), producer, consumer)); } diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupViewModel.cs new file mode 100644 index 0000000..5715927 --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationSetupViewModel.cs @@ -0,0 +1,424 @@ +using System.Globalization; +using System.IO; +using System.Net; +using OpenVisionLab; +using OpenVisionLab.Integration.Contracts; +using OpenVisionLab.Machine.Infrastructure.Integration; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +/// +/// Owns the editable Machine Studio integration setup form. It validates and +/// persists paths, consumer identity and TCP endpoints, but does not publish a +/// handoff, observe results or start a network operation. +/// +public sealed class MachineIntegrationSetupViewModel : ViewModelBase +{ + private readonly MachineIntegrationPathReadinessPolicy _pathReadiness; + private readonly MachineIntegrationSetupStore _setupStore; + private readonly Func _selectExchangeRoot; + private readonly Func _selectInspectionRecipe; + private readonly Func _canEditTcpSetup; + private readonly Action _setStatus; + private readonly Action _clearSessionSharedKey; + private string _exchangeRoot = string.Empty; + private string _inspectionRecipePath = string.Empty; + private string _twoDConsumerVersion = string.Empty; + private string _twoDConsumerCommit = string.Empty; + private string _tcpListenAddress = "127.0.0.1"; + private string _tcpListenPortText = "45101"; + private string _tcpPeerHost = "127.0.0.1"; + private string _tcpPeerPortText = "45102"; + + internal MachineIntegrationSetupViewModel( + string? settingsPath, + MachineIntegrationPathReadinessPolicy pathReadiness, + Func selectExchangeRoot, + Func selectInspectionRecipe, + Func canEditTcpSetup, + Action setStatus, + Action clearSessionSharedKey) + { + _pathReadiness = pathReadiness ?? throw new ArgumentNullException(nameof(pathReadiness)); + _setupStore = new MachineIntegrationSetupStore(settingsPath); + _selectExchangeRoot = selectExchangeRoot ?? throw new ArgumentNullException(nameof(selectExchangeRoot)); + _selectInspectionRecipe = selectInspectionRecipe ?? throw new ArgumentNullException(nameof(selectInspectionRecipe)); + _canEditTcpSetup = canEditTcpSetup ?? throw new ArgumentNullException(nameof(canEditTcpSetup)); + _setStatus = setStatus ?? throw new ArgumentNullException(nameof(setStatus)); + _clearSessionSharedKey = clearSessionSharedKey ?? throw new ArgumentNullException(nameof(clearSessionSharedKey)); + + BrowseExchangeRootCommand = new RelayCommand( + _ => BrowseExchangeRoot(), + useCommandManagerRequery: false); + BrowseRecipeCommand = new RelayCommand( + _ => BrowseRecipe(), + useCommandManagerRequery: false); + SaveSetupCommand = new RelayCommand( + _ => SaveSetup(), + _ => CanEditTcpSetup, + useCommandManagerRequery: false); + ResetSetupCommand = new RelayCommand( + _ => ResetSetup(), + _ => CanEditTcpSetup, + useCommandManagerRequery: false); + + var settingsLoad = _setupStore.Load(); + ApplySettings(settingsLoad.Settings); + _setStatus(CreateLoadStatus(settingsLoad)); + } + + public RelayCommand BrowseExchangeRootCommand { get; } + public RelayCommand BrowseRecipeCommand { get; } + public RelayCommand SaveSetupCommand { get; } + public RelayCommand ResetSetupCommand { get; } + + public string ExchangeRoot + { + get => _exchangeRoot; + set + { + if (SetProperty(ref _exchangeRoot, value ?? string.Empty)) + { + RefreshCommandState(); + OnPropertyChanged(nameof(ExchangeRootStatusText)); + OnPropertyChanged(nameof(Readiness)); + } + } + } + + public string InspectionRecipePath + { + get => _inspectionRecipePath; + set + { + if (SetProperty(ref _inspectionRecipePath, value ?? string.Empty)) + { + RefreshCommandState(); + OnPropertyChanged(nameof(InspectionRecipeStatusText)); + OnPropertyChanged(nameof(Readiness)); + } + } + } + + public string TwoDConsumerVersion + { + get => _twoDConsumerVersion; + set + { + if (SetProperty(ref _twoDConsumerVersion, value ?? string.Empty)) + { + RefreshCommandState(); + OnPropertyChanged(nameof(TwoDConsumerIdentityStatusText)); + OnPropertyChanged(nameof(TwoDConsumerIdentity)); + } + } + } + + public string TwoDConsumerCommit + { + get => _twoDConsumerCommit; + set + { + if (SetProperty(ref _twoDConsumerCommit, value ?? string.Empty)) + { + RefreshCommandState(); + OnPropertyChanged(nameof(TwoDConsumerIdentityStatusText)); + OnPropertyChanged(nameof(TwoDConsumerIdentity)); + } + } + } + + public string TcpListenAddress + { + get => _tcpListenAddress; + set + { + if (SetProperty(ref _tcpListenAddress, value ?? string.Empty)) + { + RefreshCommandState(); + } + } + } + + public string TcpListenPortText + { + get => _tcpListenPortText; + set + { + if (SetProperty(ref _tcpListenPortText, value ?? string.Empty)) + { + RefreshCommandState(); + } + } + } + + public string TcpPeerHost + { + get => _tcpPeerHost; + set + { + if (SetProperty(ref _tcpPeerHost, value ?? string.Empty)) + { + RefreshCommandState(); + } + } + } + + public string TcpPeerPortText + { + get => _tcpPeerPortText; + set + { + if (SetProperty(ref _tcpPeerPortText, value ?? string.Empty)) + { + RefreshCommandState(); + } + } + } + + public bool CanEditTcpSetup => _canEditTcpSetup(); + + public string ExchangeRootStatusText => Readiness.IsExchangeRootAvailable + ? L("Available", "교환 폴더를 사용할 수 있습니다.", "Exchange folder is available.") + : L("Unavailable", "교환 폴더를 선택하고 설정 저장을 누르세요.", "Choose an exchange folder and save setup."); + + public string InspectionRecipeStatusText => Readiness.IsInspectionRecipeAvailable + ? L("RecipeAvailable", "2D 검사 레시피 파일을 사용할 수 있습니다.", "The 2D inspection recipe file is available.") + : L("RecipeRequired", "2D 소비자가 읽을 레시피 파일을 선택하세요.", "Choose the recipe file that the 2D consumer will read."); + + public string TwoDConsumerIdentityStatusText => TwoDConsumerIdentity is not null + ? L("ConsumerIdentityValid", "2D 소비자 clean build identity가 유효합니다.", "The 2D consumer clean-build identity is valid.") + : L("ConsumerIdentityInvalid", "2D 소비자 버전과 40자리 source commit을 입력하세요.", "Enter the 2D consumer version and its 40-character source commit."); + + public IntegrationApplicationIdentity? TwoDConsumerIdentity + { + get + { + var commit = TwoDConsumerCommit.Trim(); + if (string.IsNullOrWhiteSpace(TwoDConsumerVersion) + || commit.Length != 40 + || commit.Any(character => !Uri.IsHexDigit(character))) + { + return null; + } + + return new( + IntegrationApplicationIds.TwoDStudio, + TwoDConsumerVersion.Trim(), + commit, + IntegrationSourceState.Clean); + } + } + + public MachineIntegrationPathReadinessSnapshot Readiness => + _pathReadiness.Evaluate(ExchangeRoot, InspectionRecipePath); + + internal event EventHandler? ResetCompleted; + + internal MachineIntegrationTcpSettings RequireSavedTcpSettings() + { + var root = _pathReadiness.NormalizeFullPath(RequireText( + ExchangeRoot, + L( + "ChooseAndSaveFolder", + "교환 폴더를 선택하고 저장하세요.", + "Choose and save an exchange folder."))); + var current = ResolveCurrentTcpSettings(root); + if (!_setupStore.MatchesSavedTcpSettings(current)) + { + throw new InvalidOperationException(L( + "SaveCurrentTcpSetup", + "TCP 작업 전에 현재 교환 폴더와 주소를 설정 저장하세요.", + "Save the current exchange folder and TCP endpoints before a TCP action.")); + } + + if (!_pathReadiness.IsExchangeRootAvailable(root)) + { + throw new DirectoryNotFoundException(L( + "SavedFolderUnavailable", + "저장한 교환 폴더를 사용할 수 없습니다. 폴더를 다시 선택하거나 만든 뒤 설정을 저장하세요.", + "The saved exchange folder is unavailable. Choose or recreate it, then save setup again.")); + } + + return current; + } + + internal string NormalizePath(string? path) => _pathReadiness.Normalize(path); + + internal void RefreshLocalization() + { + OnPropertyChanged(nameof(ExchangeRootStatusText)); + OnPropertyChanged(nameof(InspectionRecipeStatusText)); + OnPropertyChanged(nameof(TwoDConsumerIdentityStatusText)); + } + + internal void RefreshCommandState() + { + SaveSetupCommand.RaiseCanExecuteChanged(); + ResetSetupCommand.RaiseCanExecuteChanged(); + } + + private void BrowseExchangeRoot() + { + if (_selectExchangeRoot(ExchangeRoot) is { } selectedPath) + { + ExchangeRoot = selectedPath; + _setStatus(L( + "FolderSelected", + "교환 폴더를 선택했습니다. 설정 저장을 눌러 기억하세요.", + "Exchange folder selected. Choose Save setup to remember it.")); + } + } + + private void BrowseRecipe() + { + if (_selectInspectionRecipe(InspectionRecipePath) is { } selectedPath) + { + InspectionRecipePath = selectedPath; + _setStatus(L( + "RecipeSelected", + "2D 레시피를 선택했습니다. 설정 저장 후 Publish를 실행하세요.", + "2D recipe selected. Save setup before publishing.")); + } + } + + private void SaveSetup() + { + try + { + var root = RequireText( + ExchangeRoot, + L("RootRequired", "교환 폴더를 선택하세요.", "Choose an exchange folder.")); + var tcp = ResolveCurrentTcpSettings(root); + var settings = new MachineIntegrationSetup + { + ExchangeRoot = tcp.ExchangeRoot, + InspectionRecipePath = _pathReadiness.NormalizeFullPath(InspectionRecipePath), + TwoDConsumerVersion = TwoDConsumerVersion.Trim(), + TwoDConsumerCommit = TwoDConsumerCommit.Trim(), + TcpListenAddress = tcp.ListenAddress.ToString(), + TcpListenPort = tcp.ListenPort, + TcpPeerHost = tcp.PeerHost, + TcpPeerPort = tcp.PeerPort + }; + + _setupStore.Save(settings); + ApplySettings(settings); + _setStatus(L( + "SetupSaved", + "교환 폴더와 TCP 주소를 저장했습니다. 공유 키는 저장하지 않으며 네트워크/Publish/Refresh는 실행하지 않았습니다.", + "Exchange folder and TCP endpoints saved. The shared key is not saved; network, Publish, and Refresh were not run.")); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException) + { + _setStatus(exception.Message); + } + } + + private void ResetSetup() + { + try + { + _setupStore.Reset(); + ApplySettings(new MachineIntegrationSetup()); + _clearSessionSharedKey(); + ResetCompleted?.Invoke(this, EventArgs.Empty); + _setStatus(L( + "SetupReset", + "통합 설정을 초기화했습니다. Handoff 작업은 실행하지 않았습니다.", + "Integration setup reset. No Handoff action was run.")); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException) + { + _setStatus(exception.Message); + } + } + + private void ApplySettings(MachineIntegrationSetup settings) + { + ExchangeRoot = settings.ExchangeRoot; + InspectionRecipePath = settings.InspectionRecipePath; + TwoDConsumerVersion = settings.TwoDConsumerVersion; + TwoDConsumerCommit = settings.TwoDConsumerCommit; + TcpListenAddress = settings.TcpListenAddress; + TcpListenPortText = settings.TcpListenPort.ToString(CultureInfo.InvariantCulture); + TcpPeerHost = settings.TcpPeerHost; + TcpPeerPortText = settings.TcpPeerPort.ToString(CultureInfo.InvariantCulture); + } + + private MachineIntegrationTcpSettings ResolveCurrentTcpSettings(string exchangeRoot) + { + var listenAddressText = RequireText( + TcpListenAddress, + L( + "TcpListenAddressRequired", + "TCP 수신 주소를 입력하세요.", + "Enter a TCP listen address.")); + if (!IPAddress.TryParse(listenAddressText, out var listenAddress)) + { + throw new ArgumentException(L( + "TcpListenAddressInvalid", + "TCP 수신 주소는 이 PC의 올바른 IP 주소여야 합니다.", + "The TCP listen address must be a valid IP address on this PC.")); + } + + return new( + _pathReadiness.NormalizeFullPath(exchangeRoot), + listenAddress, + ParsePort( + TcpListenPortText, + L("TcpListenPort", "수신 포트", "listen port")), + RequireText( + TcpPeerHost, + L("TcpPeerRequired", "TCP 상대 주소를 입력하세요.", "Enter a TCP peer host.")), + ParsePort( + TcpPeerPortText, + L("TcpPeerPort", "상대 포트", "peer port"))); + } + + private static int ParsePort(string value, string name) + { + if (!int.TryParse( + value, + NumberStyles.None, + CultureInfo.InvariantCulture, + out var port) + || port is < 1 or > IPEndPoint.MaxPort) + { + throw new ArgumentException( + $"{name} must be between 1 and {IPEndPoint.MaxPort}."); + } + + return port; + } + + private static string RequireText(string value, string message) => + string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException(message) + : value.Trim(); + + private static string CreateLoadStatus(MachineIntegrationSetupLoadResult settingsLoad) => + settingsLoad.Warning switch + { + MachineIntegrationSetupLoadWarning.MissingOrInvalid => L( + "SettingsIncompatible", + "저장된 TCP 설정이 없거나 올바르지 않아 기본값을 복원했습니다. 수신은 시작되지 않았습니다.", + "Saved TCP settings were missing or invalid; defaults were restored. Listening was not started."), + MachineIntegrationSetupLoadWarning.ReadFailed => string.Format( + CultureInfo.CurrentCulture, + L( + "SettingsReadFailed", + "저장된 TCP 설정을 읽지 못해 기본값을 복원했습니다: {0}", + "Saved TCP settings could not be read; defaults were restored: {0}"), + settingsLoad.ErrorMessage ?? string.Empty), + _ => L( + "Ready", + "설정을 확인했습니다. 폴더를 스캔하거나 검사를 실행하지 않았습니다.", + "Setup loaded. No folder was scanned and no inspection was run.") + }; + + private static string L(string key, string korean, string english) => + OpenVisionLanguageService.CurrentLanguage == OpenVisionLanguage.English + ? english + : korean; +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationTcpControlViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationTcpControlViewModel.cs index d782f5f..a6fa8f7 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationTcpControlViewModel.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationTcpControlViewModel.cs @@ -21,6 +21,7 @@ internal sealed class MachineIntegrationTcpControlViewModel : ViewModelBase, IDi private readonly Func _settingsProvider; private readonly Func _latestTransactionProvider; private readonly Func _refreshResults; + private readonly Func, Task> _invokeOnUiThreadAsync; private readonly Action _setStatus; private readonly MachineIntegrationSharedKeyStore _sharedKeyStore = new(); private readonly MachineIntegrationTcpWorkflow _tcpWorkflow = new(); @@ -35,12 +36,14 @@ internal MachineIntegrationTcpControlViewModel( Func settingsProvider, Func latestTransactionProvider, Func refreshResults, - Action setStatus) + Action setStatus, + Func, Task>? invokeOnUiThreadAsync = null) { _settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider)); _latestTransactionProvider = latestTransactionProvider ?? throw new ArgumentNullException(nameof(latestTransactionProvider)); _refreshResults = refreshResults ?? throw new ArgumentNullException(nameof(refreshResults)); + _invokeOnUiThreadAsync = invokeOnUiThreadAsync ?? ExecuteImmediatelyAsync; _setStatus = setStatus ?? throw new ArgumentNullException(nameof(setStatus)); _tcpOperationOwner = new( isBusy => IsTcpBusy = isBusy, @@ -179,7 +182,7 @@ internal Task StartTcpListenerAsync() => RunTcpOperationAsync( key, cancellationToken) .ConfigureAwait(false); - _tcpOperationOwner.TryPublishIfActive(() => + await DispatchPresentationAsync(() => { IsTcpListening = true; TcpListenerStatusText = string.Format( @@ -190,7 +193,7 @@ internal Task StartTcpListenerAsync() => RunTcpOperationAsync( "TcpStarted", "TCP 수신을 시작했습니다. 수신만으로 ACK, 검사, Run 또는 Result를 실행하지 않습니다.", "TCP listening started. Receipt alone never ACKs, inspects, runs, or creates a Result.")); - }); + }).ConfigureAwait(false); } finally { @@ -203,7 +206,7 @@ internal Task StopTcpListenerAsync() => RunTcpOperationAsync( async _ => { await _tcpWorkflow.StopListeningAsync().ConfigureAwait(false); - _tcpOperationOwner.TryPublishIfActive(() => + await DispatchPresentationAsync(() => { IsTcpListening = false; TcpListenerStatusText = L( @@ -214,7 +217,7 @@ internal Task StopTcpListenerAsync() => RunTcpOperationAsync( "TcpStoppedStatus", "TCP 수신을 중지했습니다.", "TCP listening stopped.")); - }); + }).ConfigureAwait(false); }); internal Task PingTcpPeerAsync() => RunTcpTransferAsync( @@ -313,22 +316,25 @@ private Task RunTcpTransferAsync( receipt.FilesTransferred, receipt.BytesTransferred, receipt.Idempotent); - if (!_tcpOperationOwner.TryPublishIfActive(() => LastTcpTransferText = transferText)) + if (!await DispatchPresentationAsync(() => LastTcpTransferText = transferText) + .ConfigureAwait(false)) { return; } if (refreshAfterTransfer) { - if (_tcpOperationOwner.IsDisposed) + await _invokeOnUiThreadAsync(async () => { - return; - } - - await _refreshResults().ConfigureAwait(true); + if (!_tcpOperationOwner.IsDisposed) + { + await _refreshResults().ConfigureAwait(true); + } + }).ConfigureAwait(false); } - _tcpOperationOwner.TryPublishIfActive(() => _setStatus(LastTcpTransferText)); + await DispatchPresentationAsync(() => _setStatus(LastTcpTransferText)) + .ConfigureAwait(false); } finally { @@ -341,6 +347,21 @@ private Task RunTcpOperationAsync( Func operation) => _tcpOperationOwner.TrackAsync(busyStatus, operation); + private async Task DispatchPresentationAsync(Action action) + { + ArgumentNullException.ThrowIfNull(action); + + var published = false; + await _invokeOnUiThreadAsync(() => + { + published = _tcpOperationOwner.TryPublishIfActive(action); + return Task.CompletedTask; + }).ConfigureAwait(false); + return published; + } + + private static Task ExecuteImmediatelyAsync(Func operation) => operation(); + private byte[] AcquireSharedKey() { var key = _sharedKeyStore.TryAcquire(); diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationViewModel.cs index 92228a9..cc59e91 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationViewModel.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Integration/MachineIntegrationViewModel.cs @@ -1,8 +1,4 @@ -using System.Globalization; -using System.IO; -using System.Net; using System.ComponentModel; -using System.Windows.Input; using OpenVisionLab; using OpenVisionLab.Integration.Contracts; using OpenVisionLab.Machine.Infrastructure.Integration; @@ -11,9 +7,9 @@ namespace OpenVisionLab.MachineStudio.ViewModel; /// -/// Owns the explicit Machine Studio integration setup and its read-only -/// transaction projection. It never acknowledges a handoff or starts a -/// consumer inspection. +/// Composes the explicit Machine Studio integration setup with its read-only +/// transaction projection; setup editing belongs to . +/// This owner never acknowledges a handoff or starts a consumer inspection. /// public sealed class MachineIntegrationViewModel : ViewModelBase, IDisposable { @@ -22,21 +18,9 @@ public sealed class MachineIntegrationViewModel : ViewModelBase, IDisposable private readonly Func _projectIdProvider; private readonly Func? _requestContextProvider; private string? _requestContextKey; - private readonly Func _selectExchangeRoot; - private readonly Func _selectInspectionRecipe; private readonly Func, Task> _invokeOnUiThreadAsync; - private readonly MachineIntegrationPathReadinessPolicy _pathReadiness; - private readonly MachineIntegrationSetupStore _setupStore; private readonly MachineIntegrationParticipant _integrationParticipant; private readonly MachineIntegrationTcpControlViewModel _tcpControl; - private string _exchangeRoot = string.Empty; - private string _inspectionRecipePath = string.Empty; - private string _twoDConsumerVersion = string.Empty; - private string _twoDConsumerCommit = string.Empty; - private string _tcpListenAddress = "127.0.0.1"; - private string _tcpListenPortText = "45101"; - private string _tcpPeerHost = "127.0.0.1"; - private string _tcpPeerPortText = "45102"; private readonly MachineIntegrationResultObservationWorkflow _resultObservation; private string _statusText = string.Empty; private readonly object _lifetimeGate = new(); @@ -106,71 +90,42 @@ internal MachineIntegrationViewModel( _requestFactory = requestFactory ?? throw new ArgumentNullException(nameof(requestFactory)); _canBuildRequest = canBuildRequest ?? throw new ArgumentNullException(nameof(canBuildRequest)); _projectIdProvider = projectIdProvider ?? throw new ArgumentNullException(nameof(projectIdProvider)); - _selectExchangeRoot = selectExchangeRoot ?? throw new ArgumentNullException(nameof(selectExchangeRoot)); - _selectInspectionRecipe = selectInspectionRecipe ?? throw new ArgumentNullException(nameof(selectInspectionRecipe)); + ArgumentNullException.ThrowIfNull(selectExchangeRoot); + ArgumentNullException.ThrowIfNull(selectInspectionRecipe); _invokeOnUiThreadAsync = invokeOnUiThreadAsync ?? ExecuteImmediatelyAsync; - _pathReadiness = new(); - _setupStore = new(settingsPath); _integrationParticipant = new(); _integrationParticipant.StateChanged += OnIntegrationParticipantStateChanged; - var settingsLoad = _setupStore.Load(); - var settings = settingsLoad.Settings; - _exchangeRoot = settings.ExchangeRoot; - _inspectionRecipePath = settings.InspectionRecipePath; - _twoDConsumerVersion = settings.TwoDConsumerVersion; - _twoDConsumerCommit = settings.TwoDConsumerCommit; - _tcpListenAddress = settings.TcpListenAddress; - _tcpListenPortText = settings.TcpListenPort.ToString(CultureInfo.InvariantCulture); - _tcpPeerHost = settings.TcpPeerHost; - _tcpPeerPortText = settings.TcpPeerPort.ToString(CultureInfo.InvariantCulture); + MachineIntegrationTcpControlViewModel? tcpControl = null; + var pathReadiness = new MachineIntegrationPathReadinessPolicy(); + Setup = new( + settingsPath, + pathReadiness, + selectExchangeRoot, + selectInspectionRecipe, + () => !IsDisposed && (tcpControl?.CanEditTcpSetup ?? true), + status => TryPublish(() => StatusText = status), + () => tcpControl?.SetSessionSharedKey(null)); _resultObservation = new( - () => ExchangeRoot, + () => Setup.ExchangeRoot, _projectIdProvider, () => CanRefreshResults, () => IsBusy, RefreshResultsAsync, - _invokeOnUiThreadAsync, - exception => - { - TryPublish(() => StatusText = exception.Message); - }, - _pathReadiness); - _statusText = settingsLoad.Warning switch - { - MachineIntegrationSetupLoadWarning.MissingOrInvalid => L( - "SettingsIncompatible", - "저장된 TCP 설정이 없거나 올바르지 않아 기본값을 복원했습니다. 수신은 시작되지 않았습니다.", - "Saved TCP settings were missing or invalid; defaults were restored. Listening was not started."), - MachineIntegrationSetupLoadWarning.ReadFailed => string.Format( - CultureInfo.CurrentCulture, - L( - "SettingsReadFailed", - "저장된 TCP 설정을 읽지 못해 기본값을 복원했습니다: {0}", - "Saved TCP settings could not be read; defaults were restored: {0}"), - settingsLoad.ErrorMessage ?? string.Empty), - _ => L( - "Ready", - "설정을 확인했습니다. 폴더를 스캔하거나 검사를 실행하지 않았습니다.", - "Setup loaded. No folder was scanned and no inspection was run.") - }; + _invokeOnUiThreadAsync, + exception => TryPublish(() => StatusText = exception.Message), + pathReadiness); _tcpControl = new( - RequireSavedTcpSettings, + Setup.RequireSavedTcpSettings, () => _resultObservation.LatestTransaction, RefreshResultsAsync, - status => TryPublish(() => StatusText = status)); + status => TryPublish(() => StatusText = status), + _invokeOnUiThreadAsync); + tcpControl = _tcpControl; + Setup.PropertyChanged += OnSetupPropertyChanged; + Setup.ResetCompleted += OnSetupResetCompleted; _tcpControl.PropertyChanged += OnTcpControlPropertyChanged; - BrowseExchangeRootCommand = new RelayCommand(_ => BrowseExchangeRoot(), useCommandManagerRequery: false); - BrowseRecipeCommand = new RelayCommand(_ => BrowseRecipe(), useCommandManagerRequery: false); - SaveSetupCommand = new RelayCommand( - _ => SaveSetup(), - _ => CanEditTcpSetup, - useCommandManagerRequery: false); - ResetSetupCommand = new RelayCommand( - _ => ResetSetup(), - _ => CanEditTcpSetup, - useCommandManagerRequery: false); PublishTwoDImageHandoffCommand = new AsyncRelayCommand( _ => PublishTwoDImageHandoffAsync(), _ => CanPublishTwoDImageHandoff, @@ -181,13 +136,11 @@ internal MachineIntegrationViewModel( _ => CanRefreshResults, HandleCommandException, useCommandManagerRequery: false); + Setup.RefreshCommandState(); _resultObservation.ConfigureWatcher(); } - public RelayCommand BrowseExchangeRootCommand { get; } - public RelayCommand BrowseRecipeCommand { get; } - public RelayCommand SaveSetupCommand { get; } - public RelayCommand ResetSetupCommand { get; } + public MachineIntegrationSetupViewModel Setup { get; } public AsyncRelayCommand PublishTwoDImageHandoffCommand { get; } public AsyncRelayCommand RefreshResultsCommand { get; } public RelayCommand StartTcpListenerCommand => _tcpControl.StartTcpListenerCommand; @@ -196,107 +149,6 @@ internal MachineIntegrationViewModel( public RelayCommand PushLatestTransactionCommand => _tcpControl.PushLatestTransactionCommand; public RelayCommand PullLatestTransactionCommand => _tcpControl.PullLatestTransactionCommand; - public string ExchangeRoot - { - get => _exchangeRoot; - set - { - if (SetProperty(ref _exchangeRoot, value ?? string.Empty)) - { - _resultObservation.ConfigureWatcher(); - RefreshCommandState(); - OnPropertyChanged(nameof(ExchangeRootStatusText)); - } - } - } - - public string InspectionRecipePath - { - get => _inspectionRecipePath; - set - { - if (SetProperty(ref _inspectionRecipePath, value ?? string.Empty)) - { - RefreshCommandState(); - OnPropertyChanged(nameof(InspectionRecipeStatusText)); - } - } - } - - public string TwoDConsumerVersion - { - get => _twoDConsumerVersion; - set - { - if (SetProperty(ref _twoDConsumerVersion, value ?? string.Empty)) - { - RefreshCommandState(); - OnPropertyChanged(nameof(TwoDConsumerIdentityStatusText)); - } - } - } - - public string TwoDConsumerCommit - { - get => _twoDConsumerCommit; - set - { - if (SetProperty(ref _twoDConsumerCommit, value ?? string.Empty)) - { - RefreshCommandState(); - OnPropertyChanged(nameof(TwoDConsumerIdentityStatusText)); - } - } - } - - public string TcpListenAddress - { - get => _tcpListenAddress; - set - { - if (SetProperty(ref _tcpListenAddress, value ?? string.Empty)) - { - RefreshCommandState(); - } - } - } - - public string TcpListenPortText - { - get => _tcpListenPortText; - set - { - if (SetProperty(ref _tcpListenPortText, value ?? string.Empty)) - { - RefreshCommandState(); - } - } - } - - public string TcpPeerHost - { - get => _tcpPeerHost; - set - { - if (SetProperty(ref _tcpPeerHost, value ?? string.Empty)) - { - RefreshCommandState(); - } - } - } - - public string TcpPeerPortText - { - get => _tcpPeerPortText; - set - { - if (SetProperty(ref _tcpPeerPortText, value ?? string.Empty)) - { - RefreshCommandState(); - } - } - } - public bool IsBusy { get => _integrationParticipant.IsBusy; @@ -323,8 +175,8 @@ public bool CanPublishTwoDImageHandoff return false; } - var consumer = TwoDConsumerIdentity; - var paths = _pathReadiness.Evaluate(ExchangeRoot, InspectionRecipePath); + var consumer = Setup.TwoDConsumerIdentity; + var paths = Setup.Readiness; if (!_integrationParticipant.CanStart || consumer is null || !paths.CanPublishHandoff) @@ -340,24 +192,12 @@ public bool CanPublishTwoDImageHandoff !IsDisposed && _integrationParticipant.CanStart && !string.IsNullOrWhiteSpace(_projectIdProvider()) - && _pathReadiness.Evaluate(ExchangeRoot, InspectionRecipePath).CanRefreshResults; + && Setup.Readiness.CanRefreshResults; public bool CanPushLatestTransaction => _tcpControl.CanPushLatestTransaction; public bool CanPullLatestTransaction => _tcpControl.CanPullLatestTransaction; - public string ExchangeRootStatusText => _pathReadiness.Evaluate(ExchangeRoot, InspectionRecipePath).IsExchangeRootAvailable - ? L("Available", "교환 폴더를 사용할 수 있습니다.", "Exchange folder is available.") - : L("Unavailable", "교환 폴더를 선택하고 설정 저장을 누르세요.", "Choose an exchange folder and save setup."); - - public string InspectionRecipeStatusText => _pathReadiness.Evaluate(ExchangeRoot, InspectionRecipePath).IsInspectionRecipeAvailable - ? L("RecipeAvailable", "2D 검사 레시피 파일을 사용할 수 있습니다.", "The 2D inspection recipe file is available.") - : L("RecipeRequired", "2D 소비자가 읽을 레시피 파일을 선택하세요.", "Choose the recipe file that the 2D consumer will read."); - - public string TwoDConsumerIdentityStatusText => TwoDConsumerIdentity is not null - ? L("ConsumerIdentityValid", "2D 소비자 clean build identity가 유효합니다.", "The 2D consumer clean-build identity is valid.") - : L("ConsumerIdentityInvalid", "2D 소비자 버전과 40자리 source commit을 입력하세요.", "Enter the 2D consumer version and its 40-character source commit."); - public string HandoffStatusText => _resultObservation.LatestTransaction is null ? L("NoHandoff", "현재 프로젝트의 Handoff가 없습니다.", "No Handoff exists for the current project.") : string.Format( @@ -456,26 +296,6 @@ public string StatusText private set => SetProperty(ref _statusText, value); } - public IntegrationApplicationIdentity? TwoDConsumerIdentity - { - get - { - var commit = TwoDConsumerCommit.Trim(); - if (string.IsNullOrWhiteSpace(TwoDConsumerVersion) - || commit.Length != 40 - || commit.Any(character => !Uri.IsHexDigit(character))) - { - return null; - } - - return new( - IntegrationApplicationIds.TwoDStudio, - TwoDConsumerVersion.Trim(), - commit, - IntegrationSourceState.Clean); - } - } - internal void RefreshSourceContext() { if (_requestContextProvider is not { } getContext) @@ -505,9 +325,7 @@ public void RefreshContext() public void RefreshLocalization() { - OnPropertyChanged(nameof(ExchangeRootStatusText)); - OnPropertyChanged(nameof(InspectionRecipeStatusText)); - OnPropertyChanged(nameof(TwoDConsumerIdentityStatusText)); + Setup.RefreshLocalization(); _tcpControl.RefreshLocalization(); RaiseProjectionChanged(); StatusText = L( @@ -546,99 +364,10 @@ internal async Task ObserveAsync(TimeSpan t internal Task PullLatestTransactionAsync() => _tcpControl.PullLatestTransactionAsync(); - private void BrowseExchangeRoot() - { - if (_selectExchangeRoot(ExchangeRoot) is { } selectedPath) - { - ExchangeRoot = selectedPath; - StatusText = L( - "FolderSelected", - "교환 폴더를 선택했습니다. 설정 저장을 눌러 기억하세요.", - "Exchange folder selected. Choose Save setup to remember it."); - } - } - - private void BrowseRecipe() - { - if (_selectInspectionRecipe(InspectionRecipePath) is { } selectedPath) - { - InspectionRecipePath = selectedPath; - StatusText = L( - "RecipeSelected", - "2D 레시피를 선택했습니다. 설정 저장 후 Publish를 실행하세요.", - "2D recipe selected. Save setup before publishing."); - } - } - - private void SaveSetup() - { - try - { - var root = RequireText(ExchangeRoot, L("RootRequired", "교환 폴더를 선택하세요.", "Choose an exchange folder.")); - var tcp = ResolveCurrentTcpSettings(root); - Directory.CreateDirectory(tcp.ExchangeRoot); - var settings = new MachineIntegrationSetup - { - ExchangeRoot = tcp.ExchangeRoot, - InspectionRecipePath = _pathReadiness.NormalizeFullPath(InspectionRecipePath), - TwoDConsumerVersion = TwoDConsumerVersion.Trim(), - TwoDConsumerCommit = TwoDConsumerCommit.Trim(), - TcpListenAddress = tcp.ListenAddress.ToString(), - TcpListenPort = tcp.ListenPort, - TcpPeerHost = tcp.PeerHost, - TcpPeerPort = tcp.PeerPort - }; - _setupStore.Save(settings); - ExchangeRoot = settings.ExchangeRoot; - InspectionRecipePath = settings.InspectionRecipePath; - TwoDConsumerVersion = settings.TwoDConsumerVersion; - TwoDConsumerCommit = settings.TwoDConsumerCommit; - TcpListenAddress = settings.TcpListenAddress; - TcpListenPortText = settings.TcpListenPort.ToString(CultureInfo.InvariantCulture); - TcpPeerHost = settings.TcpPeerHost; - TcpPeerPortText = settings.TcpPeerPort.ToString(CultureInfo.InvariantCulture); - StatusText = L( - "SetupSaved", - "교환 폴더와 TCP 주소를 저장했습니다. 공유 키는 저장하지 않으며 네트워크/Publish/Refresh는 실행하지 않았습니다.", - "Exchange folder and TCP endpoints saved. The shared key is not saved; network, Publish, and Refresh were not run."); - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException) - { - StatusText = exception.Message; - } - } - - private void ResetSetup() - { - try - { - _setupStore.Reset(); - ExchangeRoot = string.Empty; - InspectionRecipePath = string.Empty; - TwoDConsumerVersion = string.Empty; - TwoDConsumerCommit = string.Empty; - TcpListenAddress = "127.0.0.1"; - TcpListenPortText = "45101"; - TcpPeerHost = "127.0.0.1"; - TcpPeerPortText = "45102"; - SetSessionSharedKey(null); - _resultObservation.Reset(); - RaiseProjectionChanged(); - StatusText = L( - "SetupReset", - "통합 설정을 초기화했습니다. Handoff 작업은 실행하지 않았습니다.", - "Integration setup reset. No Handoff action was run."); - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException) - { - StatusText = exception.Message; - } - } - private Task PublishTwoDImageHandoffAsync() { if (!CanPublishTwoDImageHandoff - || TwoDConsumerIdentity is not { } consumer + || Setup.TwoDConsumerIdentity is not { } consumer || !_integrationParticipant.CanStart) { return Task.FromResult(new MachineIntegrationOperationObservation( @@ -670,14 +399,14 @@ private async Task PublishTwoDImageHando { try { - var request = _requestFactory(_pathReadiness.Normalize(InspectionRecipePath), consumer) + var request = _requestFactory(Setup.NormalizePath(Setup.InspectionRecipePath), consumer) ?? throw new InvalidOperationException( L( "ContextNotReady", "저장된 프로젝트에서 완료된 가상 카메라 프레임을 먼저 준비하세요.", "Prepare a completed virtual-camera frame from a saved project first.")); - var handoff = await MachineIntegrationHandoffPublisher.PublishAsync( - _pathReadiness.Normalize(ExchangeRoot), + var handoff = await MachineIntegrationHandoffPublisher.PublishAsync( + Setup.NormalizePath(Setup.ExchangeRoot), request) .ConfigureAwait(true); TryPublish(() => @@ -754,8 +483,7 @@ private void RefreshCommandState() OnPropertyChanged(nameof(CanRefreshResults)); PublishTwoDImageHandoffCommand.RaiseCanExecuteChanged(); RefreshResultsCommand.RaiseCanExecuteChanged(); - SaveSetupCommand.RaiseCanExecuteChanged(); - ResetSetupCommand.RaiseCanExecuteChanged(); + Setup.RefreshCommandState(); _tcpControl.RefreshCommandState(); } @@ -776,6 +504,22 @@ private void OnTcpControlPropertyChanged(object? sender, PropertyChangedEventArg } } + private void OnSetupPropertyChanged(object? sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(MachineIntegrationSetupViewModel.ExchangeRoot)) + { + _resultObservation.ConfigureWatcher(); + } + + TryPublish(RefreshCommandState); + } + + private void OnSetupResetCompleted(object? sender, EventArgs args) + { + _resultObservation.Reset(); + RaiseProjectionChanged(); + } + private void OnIntegrationParticipantStateChanged(object? sender, EventArgs args) { TryPublish(() => @@ -806,85 +550,6 @@ private static MachineIntegrationParticipantOutcome CombineParticipantOutcomes( private void HandleCommandException(Exception exception) => TryPublish(() => StatusText = exception.Message); - private MachineIntegrationTcpSettings RequireSavedTcpSettings() - { - var root = _pathReadiness.NormalizeFullPath(RequireText( - ExchangeRoot, - L( - "ChooseAndSaveFolder", - "교환 폴더를 선택하고 저장하세요.", - "Choose and save an exchange folder."))); - var current = ResolveCurrentTcpSettings(root); - if (!_setupStore.MatchesSavedTcpSettings(current)) - { - throw new InvalidOperationException(L( - "SaveCurrentTcpSetup", - "TCP 작업 전에 현재 교환 폴더와 주소를 설정 저장하세요.", - "Save the current exchange folder and TCP endpoints before a TCP action.")); - } - - if (!_pathReadiness.IsExchangeRootAvailable(root)) - { - throw new DirectoryNotFoundException(L( - "SavedFolderUnavailable", - "저장한 교환 폴더를 사용할 수 없습니다. 폴더를 다시 선택하거나 만든 뒤 설정을 저장하세요.", - "The saved exchange folder is unavailable. Choose or recreate it, then save setup again.")); - } - - return current; - } - - private MachineIntegrationTcpSettings ResolveCurrentTcpSettings(string exchangeRoot) - { - var listenAddressText = RequireText( - TcpListenAddress, - L( - "TcpListenAddressRequired", - "TCP 수신 주소를 입력하세요.", - "Enter a TCP listen address.")); - if (!IPAddress.TryParse(listenAddressText, out var listenAddress)) - { - throw new ArgumentException(L( - "TcpListenAddressInvalid", - "TCP 수신 주소는 이 PC의 올바른 IP 주소여야 합니다.", - "The TCP listen address must be a valid IP address on this PC.")); - } - - return new( - _pathReadiness.NormalizeFullPath(exchangeRoot), - listenAddress, - ParsePort( - TcpListenPortText, - L("TcpListenPort", "수신 포트", "listen port")), - RequireText( - TcpPeerHost, - L("TcpPeerRequired", "TCP 상대 주소를 입력하세요.", "Enter a TCP peer host.")), - ParsePort( - TcpPeerPortText, - L("TcpPeerPort", "상대 포트", "peer port"))); - } - - private static int ParsePort(string value, string name) - { - if (!int.TryParse( - value, - NumberStyles.None, - CultureInfo.InvariantCulture, - out var port) - || port is < 1 or > IPEndPoint.MaxPort) - { - throw new ArgumentException( - $"{name} must be between 1 and {IPEndPoint.MaxPort}."); - } - - return port; - } - - private static string RequireText(string value, string message) => - string.IsNullOrWhiteSpace(value) - ? throw new ArgumentException(message) - : value.Trim(); - private static string L(string key, string korean, string english) => OpenVisionLanguageService.CurrentLanguage == OpenVisionLanguage.English ? english @@ -929,6 +594,8 @@ public void Dispose() _integrationParticipant.StateChanged -= OnIntegrationParticipantStateChanged; _integrationParticipant.Dispose(); + Setup.PropertyChanged -= OnSetupPropertyChanged; + Setup.ResetCompleted -= OnSetupResetCompleted; _tcpControl.PropertyChanged -= OnTcpControlPropertyChanged; _resultObservation.Dispose(); _tcpControl.Dispose(); diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/MainViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/MainViewModel.cs index c7144db..faf8aad 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/MainViewModel.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/MainViewModel.cs @@ -1363,7 +1363,7 @@ public DeterministicCommissioningResultHistoryEntry? SelectedCommissioningHistor public ICommand NextProcessPlanReviewStepCommand => _recipeAuthoring.ProcessPlanReview.NextStepCommand; - public ICommand ExitCommand => _exitCommand ??= CreateRelayCommand(_ => _mainWpfInteractionHost.ShutdownApplication()); + public ICommand ExitCommand => _exitCommand ??= CreateRelayCommand(_ => _mainWpfInteractionHost.RequestApplicationClose()); #endregion diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectLifecycleCoordinator.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectLifecycleCoordinator.cs index 0b46b0f..e71093e 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectLifecycleCoordinator.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectLifecycleCoordinator.cs @@ -1,5 +1,6 @@ using System.IO; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.Model; namespace OpenVisionLab.MachineStudio.ViewModel; @@ -28,6 +29,7 @@ internal sealed record ProjectSaveLifecycleResult( internal sealed class ProjectLifecycleCoordinator { private readonly ProjectDocumentStore _projectStore = new(); + private readonly ProjectDocumentFileStore _projectFileStore; private readonly ProjectDocumentSession _projectSession; private readonly ProjectDocumentOperationGate _operationGate = new(); private readonly ProjectSaveParticipant _projectSaveParticipant = new(); @@ -74,12 +76,14 @@ internal ProjectLifecycleCoordinator( _selectRecipeCopyDestination = selectRecipeCopyDestination ?? throw new ArgumentNullException(nameof(selectRecipeCopyDestination)); _startupSamplePath = NormalizePath(startupSamplePath); + _projectFileStore = new(_projectStore); _projectSession = new( _projectStore, initialProject ?? throw new ArgumentNullException(nameof(initialProject)), initialProjectPath); _projectSaveWorkflow = new( + _projectFileStore, _projectStore, () => CurrentProject, prepareProjectForSave, @@ -94,12 +98,12 @@ internal ProjectLifecycleCoordinator( getUnsavedDecision ?? throw new ArgumentNullException(nameof(getUnsavedDecision)), () => TrySaveCurrentProjectWithParticipantAsync(saveAs: false)); _projectOpenWorkflow = new( - _projectStore, + _projectFileStore, ResolveUnsavedChangesCoreAsync, ApplyOpenedProjectAsync, handleProjectOpenFailure ?? throw new ArgumentNullException(nameof(handleProjectOpenFailure))); _semiconductorRecipeCopyWorkflow = new( - _projectStore, + _projectFileStore, getRecipeOverwriteRejectedMessage ?? throw new ArgumentNullException(nameof(getRecipeOverwriteRejectedMessage))); HandleProjectSaveFailure = handleProjectSaveFailure @@ -217,7 +221,7 @@ private async Task OpenBundledSampleCoreAsync() return; } - var project = await _projectStore.LoadAsync(_startupSamplePath); + var project = await _projectFileStore.LoadAsync(_startupSamplePath); if (!await _applyProject(project)) { return; diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectOpenWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectOpenWorkflow.cs index 82e4a78..094ac27 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectOpenWorkflow.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectOpenWorkflow.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.IO; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; namespace OpenVisionLab.MachineStudio.ViewModel; @@ -10,13 +11,13 @@ namespace OpenVisionLab.MachineStudio.ViewModel; /// internal sealed class ProjectOpenWorkflow { - private readonly ProjectDocumentStore _projectStore; + private readonly ProjectDocumentFileStore _projectStore; private readonly Func> _resolveUnsavedChanges; private readonly Func> _applyOpenedProject; private readonly Action _handleLoadFailure; internal ProjectOpenWorkflow( - ProjectDocumentStore projectStore, + ProjectDocumentFileStore projectStore, Func> resolveUnsavedChanges, Func> applyOpenedProject, Action handleLoadFailure) diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectSaveWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectSaveWorkflow.cs index 5dd70b8..f2e410b 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectSaveWorkflow.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Project/ProjectSaveWorkflow.cs @@ -1,5 +1,6 @@ using System.IO; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; namespace OpenVisionLab.MachineStudio.ViewModel; @@ -9,7 +10,8 @@ namespace OpenVisionLab.MachineStudio.ViewModel; /// internal sealed class ProjectSaveWorkflow { - private readonly ProjectDocumentStore _projectStore; + private readonly ProjectDocumentFileStore _projectStore; + private readonly ProjectDocumentStore _documentStore; private readonly Func _getProject; private readonly Action _prepareProject; private readonly Action _persistScenarioBatchArtifacts; @@ -17,7 +19,8 @@ internal sealed class ProjectSaveWorkflow private readonly Action _persistVisionEvidence; internal ProjectSaveWorkflow( - ProjectDocumentStore projectStore, + ProjectDocumentFileStore projectStore, + ProjectDocumentStore documentStore, Func getProject, Action prepareProject, Action persistScenarioBatchArtifacts, @@ -25,6 +28,7 @@ internal ProjectSaveWorkflow( Action persistVisionEvidence) { _projectStore = projectStore ?? throw new ArgumentNullException(nameof(projectStore)); + _documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore)); _getProject = getProject ?? throw new ArgumentNullException(nameof(getProject)); _prepareProject = prepareProject ?? throw new ArgumentNullException(nameof(prepareProject)); _persistScenarioBatchArtifacts = persistScenarioBatchArtifacts @@ -50,7 +54,7 @@ internal async Task SaveWithReceiptAsync( ?? throw new InvalidOperationException("The current project is not available."); _prepareProject(project); var contentHash = ProjectDocumentSession.ComputeContentHash( - _projectStore.SerializeForEvidence(project)); + _documentStore.SerializeForEvidence(project)); await _projectStore.SaveAsync(project, path); var fullPath = Path.GetFullPath(path); diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/ProcessPlanReviewViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/ProcessPlanReviewViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/ProcessPlanReviewViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/ProcessPlanReviewViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeAuthoringWorkspace.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/RecipeAuthoringWorkspace.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeAuthoringWorkspace.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/RecipeAuthoringWorkspace.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeCheckpointTemplateViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/RecipeCheckpointTemplateViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeCheckpointTemplateViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/RecipeCheckpointTemplateViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeProcessBlockViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/RecipeProcessBlockViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeProcessBlockViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Authoring/RecipeProcessBlockViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionProjectApplier.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionProjectApplier.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionProjectApplier.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionProjectApplier.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionRowCatalog.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionRowCatalog.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionRowCatalog.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionRowCatalog.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionSetupWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionSetupWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionSetupWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionSetupWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionSimulationWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionSimulationWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionSimulationWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionSimulationWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionWorkbenchViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionWorkbenchViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeConnectionWorkbenchViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Connections/RecipeConnectionWorkbenchViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeDryRunPlaybackViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/DryRun/RecipeDryRunPlaybackViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeDryRunPlaybackViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/DryRun/RecipeDryRunPlaybackViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeDryRunViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/DryRun/RecipeDryRunViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeDryRunViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/DryRun/RecipeDryRunViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeSequenceStepPreviewViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/DryRun/RecipeSequenceStepPreviewViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipeSequenceStepPreviewViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/DryRun/RecipeSequenceStepPreviewViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipePackCompatibilityComparisonItemViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/RecipePackCompatibilityComparisonItemViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/RecipePackCompatibilityComparisonItemViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/RecipePackCompatibilityComparisonItemViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeCopyWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeCopyWorkflow.cs similarity index 91% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeCopyWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeCopyWorkflow.cs index 397b1b6..dc481a0 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeCopyWorkflow.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeCopyWorkflow.cs @@ -1,5 +1,6 @@ using System.IO; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; namespace OpenVisionLab.MachineStudio.ViewModel; @@ -10,11 +11,11 @@ namespace OpenVisionLab.MachineStudio.ViewModel; /// internal sealed class SemiconductorRecipeCopyWorkflow { - private readonly ProjectDocumentStore _projectStore; + private readonly ProjectDocumentFileStore _projectStore; private readonly Func _getOverwriteRejectedMessage; internal SemiconductorRecipeCopyWorkflow( - ProjectDocumentStore projectStore, + ProjectDocumentFileStore projectStore, Func getOverwriteRejectedMessage) { _projectStore = projectStore ?? throw new ArgumentNullException(nameof(projectStore)); diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryCatalog.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryCatalog.cs similarity index 84% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryCatalog.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryCatalog.cs index e9929d5..f645301 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryCatalog.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryCatalog.cs @@ -1,5 +1,5 @@ -using System.IO; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; namespace OpenVisionLab.MachineStudio.ViewModel; @@ -23,15 +23,13 @@ internal sealed record SemiconductorRecipeGalleryItemDescriptor( internal sealed class SemiconductorRecipeGalleryCatalog { - private readonly ProjectDocumentStore _projectStore = new(); + private readonly ProjectDocumentFileCatalog _projectCatalog = new(); internal IEnumerable Enumerate(string galleryPath) { - foreach (var sourcePath in Directory - .EnumerateFiles(galleryPath, "*.ovmachine") - .OrderBy(path => Path.GetFileName(path), StringComparer.OrdinalIgnoreCase)) + foreach (var entry in _projectCatalog.Enumerate(galleryPath)) { - var project = _projectStore.Load(File.ReadAllText(sourcePath)); + var project = entry.Document; var sequence = project.Sequences.FirstOrDefault(); var sensorCount = project.Devices.Count(device => device.Sensor is not null); var cylinderCount = project.Devices.Count(device => device.Cylinder is not null); @@ -50,8 +48,8 @@ internal IEnumerable Enumerate(string .ToArray(); yield return new SemiconductorRecipeGalleryItemDescriptor( - sourcePath, - Path.GetFileName(sourcePath), + entry.FullPath, + entry.FileName, project.Name, project.Schema, sequence?.Name, diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryItemViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryItemViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryItemViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryItemViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryValidationSession.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryValidationSession.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryValidationSession.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryValidationSession.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryValidationWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryValidationWorkflow.cs similarity index 95% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryValidationWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryValidationWorkflow.cs index 55e5854..bf8b59c 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryValidationWorkflow.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryValidationWorkflow.cs @@ -1,4 +1,5 @@ using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.Machine.Simulation.Sequences; namespace OpenVisionLab.MachineStudio.ViewModel; @@ -22,7 +23,7 @@ internal sealed record SemiconductorRecipeGalleryValidationResult( internal sealed class SemiconductorRecipeGalleryValidationWorkflow { - private readonly ProjectDocumentStore _projectStore = new(); + private readonly ProjectDocumentFileStore _projectStore = new(); private readonly DeterministicRecipeDryRunRunner _dryRunRunner = new(); internal async Task ValidateAsync( diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemiconductorRecipeGalleryViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Gallery/SemiconductorRecipeGalleryViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionHandoffSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionHandoffSetupViewModel.cs new file mode 100644 index 0000000..b15d363 --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionHandoffSetupViewModel.cs @@ -0,0 +1,209 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using OpenVisionLab; +using OpenVisionLab.Machine.Core.Channels; +using OpenVisionLab.Machine.Core.Devices; +using OpenVisionLab.Machine.Core.Layouts; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +public sealed class InspectionHandoffSetupViewModel : ViewModelBase +{ + private readonly Func _applySetup; + private readonly Action _clearWorkbenchPreviews; + private readonly RelayCommand _previewCommand; + private readonly RelayCommand _applyCommand; + private readonly RelayCommand _cancelCommand; + private readonly RelayCommand _resetCommand; + private MachineProjectDocument? _project; + private bool _isEditable = true; + private bool _isVisible; + private InspectionHandoffDefinition? _savedSetup; + private string? _cameraId; + private string? _positionSensorChannelId; + private string? _acceptedChannelId; + private string? _readyChannelId; + private string? _completeChannelId; + + public InspectionHandoffSetupViewModel(Func applySetup, Action clearWorkbenchPreviews) + { + _applySetup = applySetup; + _clearWorkbenchPreviews = clearWorkbenchPreviews; + _previewCommand = new RelayCommand(_ => Preview(), _ => IsEditable && _project is not null); + _applyCommand = new RelayCommand(_ => Apply(), ignored => IsEditable && IsVisible && TryCreate(out _)); + _cancelCommand = new RelayCommand(_ => ClearPreviewForCompetingSetup(), _ => IsVisible); + _resetCommand = new RelayCommand(_ => Reset(), _ => IsEditable && IsVisible); + } + + public ObservableCollection CameraOptions { get; } = new(); + public ObservableCollection InputOptions { get; } = new(); + public ObservableCollection OutputOptions { get; } = new(); + + public ICommand PreviewCommand => _previewCommand; + public ICommand ApplyCommand => _applyCommand; + public ICommand CancelCommand => _cancelCommand; + public ICommand ResetCommand => _resetCommand; + + public bool IsEditable + { + get => _isEditable; + set + { + if (!SetProperty(ref _isEditable, value)) return; + RaiseCommandStates(); + } + } + + public bool IsVisible => _isVisible; + public string? CameraId { get => _cameraId; set => SetSelection(ref _cameraId, value, nameof(CameraId)); } + public string? PositionSensorChannelId { get => _positionSensorChannelId; set => SetSelection(ref _positionSensorChannelId, value, nameof(PositionSensorChannelId)); } + public string? AcceptedChannelId { get => _acceptedChannelId; set => SetSelection(ref _acceptedChannelId, value, nameof(AcceptedChannelId)); } + public string? ReadyChannelId { get => _readyChannelId; set => SetSelection(ref _readyChannelId, value, nameof(ReadyChannelId)); } + public string? CompleteChannelId { get => _completeChannelId; set => SetSelection(ref _completeChannelId, value, nameof(CompleteChannelId)); } + public bool IsCameraValid => IsCamera(CameraId); + public bool IsPositionValid => IsChannel(PositionSensorChannelId, ChannelKind.DigitalInput); + public bool IsAcceptedValid => IsChannel(AcceptedChannelId, ChannelKind.DigitalOutput); + public bool IsReadyValid => IsChannel(ReadyChannelId, ChannelKind.DigitalInput); + public bool IsCompleteValid => IsChannel(CompleteChannelId, ChannelKind.DigitalInput); + public bool HasMultipleHandoffs => _project?.Devices.Count(device => device.Kind == DeviceKind.Inspection) > 1; + public bool HasValidationError => !TryCreate(out _); + public string ValidationText => OpenVisionLanguageService.T( + HasMultipleHandoffs ? "Connections.InspectionHandoffSetupMultipleError" : HasValidationError + ? "Connections.InspectionHandoffSetupValidationError" + : "Connections.InspectionHandoffSetupValidationReady"); + + public void Load(MachineProjectDocument project) + { + _project = project ?? throw new ArgumentNullException(nameof(project)); + ClearPreviewForCompetingSetup(); + RaiseCommandStates(); + } + + internal void ClearPreviewForCompetingSetup() + { + _isVisible = false; + _savedSetup = null; + CameraOptions.Clear(); InputOptions.Clear(); OutputOptions.Clear(); + RaiseChanged(); + } + + internal void RefreshLocalization(Action reloadWorkbench) + { + var draft = IsVisible ? CaptureDraft() : null; + reloadWorkbench(); + if (draft is not null) + { + Preview(); + RestoreDraft(draft); + } + } + + private void Preview() + { + if (_project is null) return; + _clearWorkbenchPreviews(); + CameraOptions.Clear(); InputOptions.Clear(); OutputOptions.Clear(); + foreach (var camera in _project.Devices.Where(device => device is { Kind: DeviceKind.Camera, Camera: not null }).OrderBy(device => device.Name, StringComparer.CurrentCulture).ThenBy(device => device.Id, StringComparer.Ordinal)) + CameraOptions.Add(Option(camera.Id, camera.Name)); + foreach (var channel in _project.Channels.OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) + { + var option = Option(channel.Id, channel.Name); + if (channel.Kind == ChannelKind.DigitalInput) InputOptions.Add(option); + if (channel.Kind == ChannelKind.DigitalOutput) OutputOptions.Add(option); + } + var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Inspection, InspectionHandoff: not null }).ToArray(); + _savedSetup = existing.Length == 1 ? Clone(existing[0].InspectionHandoff!) : null; + var draft = _savedSetup ?? Suggest(); + AddMissing(CameraOptions, draft.CameraId); + foreach (var id in new[] { draft.InspectionPositionSensorChannelId, draft.InspectionReadyFeedbackChannelId, draft.InspectionCompleteFeedbackChannelId }) AddMissing(InputOptions, id); + AddMissing(OutputOptions, draft.ResultAcceptedCommandChannelId); + ApplyDraft(draft); + _isVisible = true; + RaiseChanged(); + } + + private void Apply() + { + if (!TryCreate(out var setup)) return; + if (_applySetup(setup) > 0 || IsEquivalentToSaved(setup)) ClearPreviewForCompetingSetup(); else Preview(); + } + + private void Reset() => ApplyDraft(_savedSetup is null ? Suggest() : Clone(_savedSetup)); + + private InspectionHandoffDefinition Suggest() => new() + { + CameraId = CameraOptions.FirstOrDefault()?.Id ?? string.Empty, + InspectionPositionSensorChannelId = InputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + InspectionReadyFeedbackChannelId = InputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + InspectionCompleteFeedbackChannelId = InputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, + ResultAcceptedCommandChannelId = OutputOptions.FirstOrDefault()?.Id ?? string.Empty + }; + + private void ApplyDraft(InspectionHandoffDefinition setup) + { + _cameraId = setup.CameraId; _positionSensorChannelId = setup.InspectionPositionSensorChannelId; _acceptedChannelId = setup.ResultAcceptedCommandChannelId; + _readyChannelId = setup.InspectionReadyFeedbackChannelId; _completeChannelId = setup.InspectionCompleteFeedbackChannelId; + RaiseValidationChanged(); + } + + private bool TryCreate(out InspectionHandoffDefinition setup) + { + var channels = new[] { PositionSensorChannelId, AcceptedChannelId, ReadyChannelId, CompleteChannelId }; + setup = new InspectionHandoffDefinition + { + CameraId = CameraId ?? string.Empty, InspectionPositionSensorChannelId = PositionSensorChannelId ?? string.Empty, + ResultAcceptedCommandChannelId = AcceptedChannelId ?? string.Empty, InspectionReadyFeedbackChannelId = ReadyChannelId ?? string.Empty, + InspectionCompleteFeedbackChannelId = CompleteChannelId ?? string.Empty + }; + return !HasMultipleHandoffs && IsCameraValid && IsPositionValid && IsAcceptedValid && IsReadyValid && IsCompleteValid && Distinct(channels); + } + + private bool IsEquivalentToSaved(InspectionHandoffDefinition setup) => _savedSetup is not null + && Same(_savedSetup.CameraId, setup.CameraId) && Same(_savedSetup.InspectionPositionSensorChannelId, setup.InspectionPositionSensorChannelId) + && Same(_savedSetup.ResultAcceptedCommandChannelId, setup.ResultAcceptedCommandChannelId) && Same(_savedSetup.InspectionReadyFeedbackChannelId, setup.InspectionReadyFeedbackChannelId) + && Same(_savedSetup.InspectionCompleteFeedbackChannelId, setup.InspectionCompleteFeedbackChannelId); + + private InspectionHandoffDraft CaptureDraft() => new(CameraId, PositionSensorChannelId, AcceptedChannelId, ReadyChannelId, CompleteChannelId); + + private void RestoreDraft(InspectionHandoffDraft draft) + { + AddMissing(CameraOptions, draft.CameraId); foreach (var id in new[] { draft.PositionInputId, draft.ReadyInputId, draft.CompleteInputId }) AddMissing(InputOptions, id); AddMissing(OutputOptions, draft.AcceptedOutputId); + _cameraId = draft.CameraId; _positionSensorChannelId = draft.PositionInputId; _acceptedChannelId = draft.AcceptedOutputId; _readyChannelId = draft.ReadyInputId; _completeChannelId = draft.CompleteInputId; + RaiseValidationChanged(); + } + + private void SetSelection(ref string? field, string? value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private bool IsCamera(string? id) => _project?.Devices.Any(device => device is { Kind: DeviceKind.Camera, Camera: not null } && Same(device.Id, id)) == true; + private bool IsChannel(string? id, ChannelKind kind) => _project?.Channels.Any(channel => channel.Kind == kind && Same(channel.Id, id)) == true; + + private void RaiseChanged() + { + OnPropertyChanged(nameof(IsVisible)); + RaiseValidationChanged(); + _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private void RaiseValidationChanged() + { + foreach (var property in ValidationProperties) OnPropertyChanged(property); + _applyCommand.RaiseCanExecuteChanged(); + } + + private void RaiseCommandStates() + { + _previewCommand.RaiseCanExecuteChanged(); _applyCommand.RaiseCanExecuteChanged(); _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private static bool Distinct(string?[] values) => values.All(value => !string.IsNullOrWhiteSpace(value)) && values.Distinct(StringComparer.Ordinal).Count() == values.Length; + private static bool Same(string? left, string? right) => string.Equals(left, right, StringComparison.Ordinal); + private static LoadLockSetupOption Option(string id, string? name) => new(id, string.IsNullOrWhiteSpace(name) ? id : $"{name} — {id}"); + private static void AddMissing(ObservableCollection options, string? id) { if (!string.IsNullOrWhiteSpace(id) && options.All(option => !Same(option.Id, id))) options.Add(new LoadLockSetupOption(id, $"{id} ({OpenVisionLanguageService.T("Connections.LoadLockSetupMissing")})")); } + private static InspectionHandoffDefinition Clone(InspectionHandoffDefinition value) => new() { CameraId = value.CameraId, InspectionPositionSensorChannelId = value.InspectionPositionSensorChannelId, ResultAcceptedCommandChannelId = value.ResultAcceptedCommandChannelId, InspectionReadyFeedbackChannelId = value.InspectionReadyFeedbackChannelId, InspectionCompleteFeedbackChannelId = value.InspectionCompleteFeedbackChannelId }; + + private sealed record InspectionHandoffDraft(string? CameraId, string? PositionInputId, string? AcceptedOutputId, string? ReadyInputId, string? CompleteInputId); + private static readonly string[] ValidationProperties = + [ + nameof(CameraId), nameof(PositionSensorChannelId), nameof(AcceptedChannelId), nameof(ReadyChannelId), nameof(CompleteChannelId), nameof(IsCameraValid), nameof(IsPositionValid), nameof(IsAcceptedValid), nameof(IsReadyValid), nameof(IsCompleteValid), nameof(HasMultipleHandoffs), nameof(HasValidationError), nameof(ValidationText) + ]; +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionSortRouterSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionSortRouterSetupViewModel.cs new file mode 100644 index 0000000..915d835 --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/InspectionSortRouterSetupViewModel.cs @@ -0,0 +1,202 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using OpenVisionLab; +using OpenVisionLab.Machine.Core.Channels; +using OpenVisionLab.Machine.Core.Devices; +using OpenVisionLab.Machine.Core.Layouts; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +public sealed class InspectionSortRouterSetupViewModel : ViewModelBase +{ + private readonly Func _applySetup; + private readonly Action _clearWorkbenchPreviews; + private readonly RelayCommand _previewCommand; + private readonly RelayCommand _applyCommand; + private readonly RelayCommand _cancelCommand; + private readonly RelayCommand _resetCommand; + private MachineProjectDocument? _project; + private bool _isEditable = true; + private bool _isVisible; + private InspectionSortRouterDefinition? _savedSetup; + private string? _cameraId; + private string? _passConveyorId; + private string? _ngConveyorId; + private string? _passFeedbackChannelId; + private string? _ngFeedbackChannelId; + + public InspectionSortRouterSetupViewModel(Func applySetup, Action clearWorkbenchPreviews) + { + _applySetup = applySetup; + _clearWorkbenchPreviews = clearWorkbenchPreviews; + _previewCommand = new RelayCommand(_ => Preview(), _ => IsEditable && _project is not null); + _applyCommand = new RelayCommand(_ => Apply(), ignored => IsEditable && IsVisible && TryCreate(out _)); + _cancelCommand = new RelayCommand(_ => ClearPreviewForCompetingSetup(), _ => IsVisible); + _resetCommand = new RelayCommand(_ => Reset(), _ => IsEditable && IsVisible); + } + + public ObservableCollection CameraOptions { get; } = new(); + public ObservableCollection ConveyorOptions { get; } = new(); + public ObservableCollection InputOptions { get; } = new(); + + public ICommand PreviewCommand => _previewCommand; + public ICommand ApplyCommand => _applyCommand; + public ICommand CancelCommand => _cancelCommand; + public ICommand ResetCommand => _resetCommand; + + public bool IsEditable + { + get => _isEditable; + set + { + if (!SetProperty(ref _isEditable, value)) return; + RaiseCommandStates(); + } + } + + public bool IsVisible => _isVisible; + public string? CameraId { get => _cameraId; set => SetSelection(ref _cameraId, value, nameof(CameraId)); } + public string? PassConveyorId { get => _passConveyorId; set => SetSelection(ref _passConveyorId, value, nameof(PassConveyorId)); } + public string? NgConveyorId { get => _ngConveyorId; set => SetSelection(ref _ngConveyorId, value, nameof(NgConveyorId)); } + public string? PassFeedbackChannelId { get => _passFeedbackChannelId; set => SetSelection(ref _passFeedbackChannelId, value, nameof(PassFeedbackChannelId)); } + public string? NgFeedbackChannelId { get => _ngFeedbackChannelId; set => SetSelection(ref _ngFeedbackChannelId, value, nameof(NgFeedbackChannelId)); } + public bool IsCameraValid => IsCamera(CameraId); + public bool IsPassConveyorValid => IsLayoutComponent(PassConveyorId, LayoutComponentKind.Conveyor); + public bool IsNgConveyorValid => IsLayoutComponent(NgConveyorId, LayoutComponentKind.Conveyor) && !Same(PassConveyorId, NgConveyorId); + public bool IsPassFeedbackValid => IsChannel(PassFeedbackChannelId, ChannelKind.DigitalInput); + public bool IsNgFeedbackValid => IsChannel(NgFeedbackChannelId, ChannelKind.DigitalInput) && !Same(PassFeedbackChannelId, NgFeedbackChannelId); + public bool HasMultipleSortRouters => _project?.Devices.Count(device => device.Kind == DeviceKind.Sorter) > 1; + public bool HasValidationError => !TryCreate(out _); + public string ValidationText => OpenVisionLanguageService.T( + HasMultipleSortRouters ? "Connections.InspectionSortSetupMultipleError" : HasValidationError + ? "Connections.InspectionSortSetupValidationError" + : "Connections.InspectionSortSetupValidationReady"); + + public void Load(MachineProjectDocument project) + { + _project = project ?? throw new ArgumentNullException(nameof(project)); + ClearPreviewForCompetingSetup(); + RaiseCommandStates(); + } + + internal void ClearPreviewForCompetingSetup() + { + _isVisible = false; + _savedSetup = null; + CameraOptions.Clear(); ConveyorOptions.Clear(); InputOptions.Clear(); + RaiseChanged(); + } + + internal void RefreshLocalization(Action reloadWorkbench) + { + var draft = IsVisible ? CaptureDraft() : null; + reloadWorkbench(); + if (draft is not null) + { + Preview(); + RestoreDraft(draft); + } + } + + private void Preview() + { + if (_project is null) return; + _clearWorkbenchPreviews(); + CameraOptions.Clear(); ConveyorOptions.Clear(); InputOptions.Clear(); + var layout = ResolveActiveLayout(_project); + foreach (var camera in _project.Devices.Where(device => device is { Kind: DeviceKind.Camera, Camera: not null }).OrderBy(device => device.Name, StringComparer.CurrentCulture).ThenBy(device => device.Id, StringComparer.Ordinal)) CameraOptions.Add(Option(camera.Id, camera.Name)); + foreach (var component in (layout?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.Conveyor).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) ConveyorOptions.Add(Option(component.Id, component.Name)); + foreach (var channel in _project.Channels.Where(channel => channel.Kind == ChannelKind.DigitalInput).OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) InputOptions.Add(Option(channel.Id, channel.Name)); + var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Sorter, InspectionSortRouter: not null }).ToArray(); + _savedSetup = existing.Length == 1 ? Clone(existing[0].InspectionSortRouter!) : null; + var draft = _savedSetup ?? Suggest(); + AddMissing(CameraOptions, draft.CameraId); AddMissing(ConveyorOptions, draft.PassConveyorComponentId); AddMissing(ConveyorOptions, draft.NgConveyorComponentId); AddMissing(InputOptions, draft.PassRoutedFeedbackChannelId); AddMissing(InputOptions, draft.NgRoutedFeedbackChannelId); + ApplyDraft(draft); + _isVisible = true; + RaiseChanged(); + } + + private void Apply() + { + if (!TryCreate(out var setup)) return; + if (_applySetup(setup) > 0 || IsEquivalentToSaved(setup)) ClearPreviewForCompetingSetup(); else Preview(); + } + + private void Reset() => ApplyDraft(_savedSetup is null ? Suggest() : Clone(_savedSetup)); + + private InspectionSortRouterDefinition Suggest() => new() + { + CameraId = CameraOptions.FirstOrDefault()?.Id ?? string.Empty, + PassConveyorComponentId = ConveyorOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + NgConveyorComponentId = ConveyorOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + PassRoutedFeedbackChannelId = InputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + NgRoutedFeedbackChannelId = InputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty + }; + + private void ApplyDraft(InspectionSortRouterDefinition setup) + { + _cameraId = setup.CameraId; _passConveyorId = setup.PassConveyorComponentId; _ngConveyorId = setup.NgConveyorComponentId; + _passFeedbackChannelId = setup.PassRoutedFeedbackChannelId; _ngFeedbackChannelId = setup.NgRoutedFeedbackChannelId; + RaiseValidationChanged(); + } + + private bool TryCreate(out InspectionSortRouterDefinition setup) + { + setup = new InspectionSortRouterDefinition + { + CameraId = CameraId ?? string.Empty, PassConveyorComponentId = PassConveyorId ?? string.Empty, NgConveyorComponentId = NgConveyorId ?? string.Empty, + PassRoutedFeedbackChannelId = PassFeedbackChannelId ?? string.Empty, NgRoutedFeedbackChannelId = NgFeedbackChannelId ?? string.Empty + }; + return !HasMultipleSortRouters && IsCameraValid && IsPassConveyorValid && IsNgConveyorValid && IsPassFeedbackValid && IsNgFeedbackValid; + } + + private bool IsEquivalentToSaved(InspectionSortRouterDefinition setup) => _savedSetup is not null + && Same(_savedSetup.CameraId, setup.CameraId) && Same(_savedSetup.PassConveyorComponentId, setup.PassConveyorComponentId) && Same(_savedSetup.NgConveyorComponentId, setup.NgConveyorComponentId) + && Same(_savedSetup.PassRoutedFeedbackChannelId, setup.PassRoutedFeedbackChannelId) && Same(_savedSetup.NgRoutedFeedbackChannelId, setup.NgRoutedFeedbackChannelId); + + private InspectionSortRouterDraft CaptureDraft() => new(CameraId, PassConveyorId, NgConveyorId, PassFeedbackChannelId, NgFeedbackChannelId); + + private void RestoreDraft(InspectionSortRouterDraft draft) + { + AddMissing(CameraOptions, draft.CameraId); AddMissing(ConveyorOptions, draft.PassConveyorId); AddMissing(ConveyorOptions, draft.NgConveyorId); AddMissing(InputOptions, draft.PassFeedbackInputId); AddMissing(InputOptions, draft.NgFeedbackInputId); + _cameraId = draft.CameraId; _passConveyorId = draft.PassConveyorId; _ngConveyorId = draft.NgConveyorId; _passFeedbackChannelId = draft.PassFeedbackInputId; _ngFeedbackChannelId = draft.NgFeedbackInputId; + RaiseValidationChanged(); + } + + private void SetSelection(ref string? field, string? value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private MachineLayoutDefinition? ActiveLayout() => _project is null ? null : ResolveActiveLayout(_project); + private bool IsLayoutComponent(string? id, LayoutComponentKind kind) => ActiveLayout()?.Components.Any(component => component.Kind == kind && Same(component.Id, id)) == true; + private bool IsChannel(string? id, ChannelKind kind) => _project?.Channels.Any(channel => channel.Kind == kind && Same(channel.Id, id)) == true; + private bool IsCamera(string? id) => _project?.Devices.Any(device => device is { Kind: DeviceKind.Camera, Camera: not null } && Same(device.Id, id)) == true; + + private void RaiseChanged() + { + OnPropertyChanged(nameof(IsVisible)); + RaiseValidationChanged(); + _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private void RaiseValidationChanged() + { + foreach (var property in ValidationProperties) OnPropertyChanged(property); + _applyCommand.RaiseCanExecuteChanged(); + } + + private void RaiseCommandStates() + { + _previewCommand.RaiseCanExecuteChanged(); _applyCommand.RaiseCanExecuteChanged(); _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private static bool Same(string? left, string? right) => string.Equals(left, right, StringComparison.Ordinal); + private static LoadLockSetupOption Option(string id, string? name) => new(id, string.IsNullOrWhiteSpace(name) ? id : $"{name} — {id}"); + private static void AddMissing(ObservableCollection options, string? id) { if (!string.IsNullOrWhiteSpace(id) && options.All(option => !Same(option.Id, id))) options.Add(new LoadLockSetupOption(id, $"{id} ({OpenVisionLanguageService.T("Connections.LoadLockSetupMissing")})")); } + private static MachineLayoutDefinition? ResolveActiveLayout(MachineProjectDocument project) => !string.IsNullOrWhiteSpace(project.Simulation.ActiveLayoutId) ? project.Layouts.FirstOrDefault(layout => Same(layout.Id, project.Simulation.ActiveLayoutId)) : project.Layouts.Count == 1 ? project.Layouts[0] : null; + private static InspectionSortRouterDefinition Clone(InspectionSortRouterDefinition value) => new() { CameraId = value.CameraId, PassConveyorComponentId = value.PassConveyorComponentId, NgConveyorComponentId = value.NgConveyorComponentId, PassRoutedFeedbackChannelId = value.PassRoutedFeedbackChannelId, NgRoutedFeedbackChannelId = value.NgRoutedFeedbackChannelId }; + + private sealed record InspectionSortRouterDraft(string? CameraId, string? PassConveyorId, string? NgConveyorId, string? PassFeedbackInputId, string? NgFeedbackInputId); + private static readonly string[] ValidationProperties = + [ + nameof(CameraId), nameof(PassConveyorId), nameof(NgConveyorId), nameof(PassFeedbackChannelId), nameof(NgFeedbackChannelId), nameof(IsCameraValid), nameof(IsPassConveyorValid), nameof(IsNgConveyorValid), nameof(IsPassFeedbackValid), nameof(IsNgFeedbackValid), nameof(HasMultipleSortRouters), nameof(HasValidationError), nameof(ValidationText) + ]; +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/OhtHandoffSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/OhtHandoffSetupViewModel.cs new file mode 100644 index 0000000..fb9bf01 --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/OhtHandoffSetupViewModel.cs @@ -0,0 +1,215 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using OpenVisionLab; +using OpenVisionLab.Machine.Core.Channels; +using OpenVisionLab.Machine.Core.Devices; +using OpenVisionLab.Machine.Core.Layouts; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +public sealed class OhtHandoffSetupViewModel : ViewModelBase +{ + private readonly Func _applySetup; + private readonly Action _clearWorkbenchPreviews; + private readonly RelayCommand _previewCommand; + private readonly RelayCommand _applyCommand; + private readonly RelayCommand _cancelCommand; + private readonly RelayCommand _resetCommand; + private MachineProjectDocument? _project; + private bool _isEditable = true; + private bool _isVisible; + private OhtHandoffDefinition? _savedSetup; + private string? _transportConveyorId; + private string? _routeAvailableChannelId; + private string? _vehicleDockedChannelId; + private string? _loadPortReadyChannelId; + private string? _carrierReceivedChannelId; + private string? _handoffReadyChannelId; + private string? _carrierTransferredChannelId; + + public OhtHandoffSetupViewModel(Func applySetup, Action clearWorkbenchPreviews) + { + _applySetup = applySetup; + _clearWorkbenchPreviews = clearWorkbenchPreviews; + _previewCommand = new RelayCommand(_ => Preview(), _ => IsEditable && _project is not null); + _applyCommand = new RelayCommand(_ => Apply(), ignored => IsEditable && IsVisible && TryCreate(out _)); + _cancelCommand = new RelayCommand(_ => ClearPreviewForCompetingSetup(), _ => IsVisible); + _resetCommand = new RelayCommand(_ => Reset(), _ => IsEditable && IsVisible); + } + + public ObservableCollection ConveyorOptions { get; } = new(); + public ObservableCollection InputOptions { get; } = new(); + + public ICommand PreviewCommand => _previewCommand; + public ICommand ApplyCommand => _applyCommand; + public ICommand CancelCommand => _cancelCommand; + public ICommand ResetCommand => _resetCommand; + + public bool IsEditable + { + get => _isEditable; + set + { + if (!SetProperty(ref _isEditable, value)) return; + RaiseCommandStates(); + } + } + + public bool IsVisible => _isVisible; + public string? TransportConveyorId { get => _transportConveyorId; set => SetSelection(ref _transportConveyorId, value, nameof(TransportConveyorId)); } + public string? RouteAvailableChannelId { get => _routeAvailableChannelId; set => SetSelection(ref _routeAvailableChannelId, value, nameof(RouteAvailableChannelId)); } + public string? VehicleDockedChannelId { get => _vehicleDockedChannelId; set => SetSelection(ref _vehicleDockedChannelId, value, nameof(VehicleDockedChannelId)); } + public string? LoadPortReadyChannelId { get => _loadPortReadyChannelId; set => SetSelection(ref _loadPortReadyChannelId, value, nameof(LoadPortReadyChannelId)); } + public string? CarrierReceivedChannelId { get => _carrierReceivedChannelId; set => SetSelection(ref _carrierReceivedChannelId, value, nameof(CarrierReceivedChannelId)); } + public string? HandoffReadyChannelId { get => _handoffReadyChannelId; set => SetSelection(ref _handoffReadyChannelId, value, nameof(HandoffReadyChannelId)); } + public string? CarrierTransferredChannelId { get => _carrierTransferredChannelId; set => SetSelection(ref _carrierTransferredChannelId, value, nameof(CarrierTransferredChannelId)); } + public bool IsTransportConveyorValid => IsLayoutComponent(TransportConveyorId, LayoutComponentKind.Conveyor); + public bool IsRouteAvailableValid => IsChannel(RouteAvailableChannelId, ChannelKind.DigitalInput); + public bool IsVehicleDockedValid => IsChannel(VehicleDockedChannelId, ChannelKind.DigitalInput); + public bool IsLoadPortReadyValid => IsChannel(LoadPortReadyChannelId, ChannelKind.DigitalInput); + public bool IsCarrierReceivedValid => IsChannel(CarrierReceivedChannelId, ChannelKind.DigitalInput); + public bool IsHandoffReadyValid => IsChannel(HandoffReadyChannelId, ChannelKind.DigitalInput); + public bool IsCarrierTransferredValid => IsChannel(CarrierTransferredChannelId, ChannelKind.DigitalInput); + public bool HasMultipleOhtHandoffs => _project?.Devices.Count(device => device.Kind == DeviceKind.Oht) > 1; + public bool HasValidationError => !TryCreate(out _); + public string ValidationText => OpenVisionLanguageService.T( + HasMultipleOhtHandoffs ? "Connections.OhtSetupMultipleError" : HasValidationError + ? "Connections.OhtSetupValidationError" + : "Connections.OhtSetupValidationReady"); + + public void Load(MachineProjectDocument project) + { + _project = project ?? throw new ArgumentNullException(nameof(project)); + ClearPreviewForCompetingSetup(); + RaiseCommandStates(); + } + + internal void ClearPreviewForCompetingSetup() + { + _isVisible = false; + _savedSetup = null; + ConveyorOptions.Clear(); InputOptions.Clear(); + RaiseChanged(); + } + + internal void RefreshLocalization(Action reloadWorkbench) + { + var draft = IsVisible ? CaptureDraft() : null; + reloadWorkbench(); + if (draft is not null) + { + Preview(); + RestoreDraft(draft); + } + } + + private void Preview() + { + if (_project is null) return; + _clearWorkbenchPreviews(); + ConveyorOptions.Clear(); InputOptions.Clear(); + var layout = ResolveActiveLayout(_project); + foreach (var component in (layout?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.Conveyor).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) ConveyorOptions.Add(Option(component.Id, component.Name)); + foreach (var channel in _project.Channels.Where(channel => channel.Kind == ChannelKind.DigitalInput).OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) InputOptions.Add(Option(channel.Id, channel.Name)); + var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Oht, OhtHandoff: not null }).ToArray(); + _savedSetup = existing.Length == 1 ? Clone(existing[0].OhtHandoff!) : null; + var draft = _savedSetup ?? Suggest(); + AddMissing(ConveyorOptions, draft.TransportConveyorComponentId); + foreach (var id in new[] { draft.RouteAvailableSensorChannelId, draft.VehicleDockedSensorChannelId, draft.LoadPortReadySensorChannelId, draft.CarrierReceivedSensorChannelId, draft.HandoffReadyFeedbackChannelId, draft.CarrierTransferredFeedbackChannelId }) AddMissing(InputOptions, id); + ApplyDraft(draft); + _isVisible = true; + RaiseChanged(); + } + + private void Apply() + { + if (!TryCreate(out var setup)) return; + if (_applySetup(setup) > 0 || IsEquivalentToSaved(setup)) ClearPreviewForCompetingSetup(); else Preview(); + } + + private void Reset() => ApplyDraft(_savedSetup is null ? Suggest() : Clone(_savedSetup)); + + private OhtHandoffDefinition Suggest() => new() + { + TransportConveyorComponentId = ConveyorOptions.FirstOrDefault()?.Id ?? string.Empty, + RouteAvailableSensorChannelId = InputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + VehicleDockedSensorChannelId = InputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + LoadPortReadySensorChannelId = InputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, + CarrierReceivedSensorChannelId = InputOptions.ElementAtOrDefault(3)?.Id ?? string.Empty, + HandoffReadyFeedbackChannelId = InputOptions.ElementAtOrDefault(4)?.Id ?? string.Empty, + CarrierTransferredFeedbackChannelId = InputOptions.ElementAtOrDefault(5)?.Id ?? string.Empty + }; + + private void ApplyDraft(OhtHandoffDefinition setup) + { + _transportConveyorId = setup.TransportConveyorComponentId; _routeAvailableChannelId = setup.RouteAvailableSensorChannelId; _vehicleDockedChannelId = setup.VehicleDockedSensorChannelId; + _loadPortReadyChannelId = setup.LoadPortReadySensorChannelId; _carrierReceivedChannelId = setup.CarrierReceivedSensorChannelId; _handoffReadyChannelId = setup.HandoffReadyFeedbackChannelId; _carrierTransferredChannelId = setup.CarrierTransferredFeedbackChannelId; + RaiseValidationChanged(); + } + + private bool TryCreate(out OhtHandoffDefinition setup) + { + var channels = new[] { RouteAvailableChannelId, VehicleDockedChannelId, LoadPortReadyChannelId, CarrierReceivedChannelId, HandoffReadyChannelId, CarrierTransferredChannelId }; + setup = new OhtHandoffDefinition + { + TransportConveyorComponentId = TransportConveyorId ?? string.Empty, RouteAvailableSensorChannelId = RouteAvailableChannelId ?? string.Empty, + VehicleDockedSensorChannelId = VehicleDockedChannelId ?? string.Empty, LoadPortReadySensorChannelId = LoadPortReadyChannelId ?? string.Empty, + CarrierReceivedSensorChannelId = CarrierReceivedChannelId ?? string.Empty, HandoffReadyFeedbackChannelId = HandoffReadyChannelId ?? string.Empty, + CarrierTransferredFeedbackChannelId = CarrierTransferredChannelId ?? string.Empty + }; + return !HasMultipleOhtHandoffs && IsTransportConveyorValid && Distinct(channels) && IsRouteAvailableValid && IsVehicleDockedValid && IsLoadPortReadyValid && IsCarrierReceivedValid && IsHandoffReadyValid && IsCarrierTransferredValid; + } + + private bool IsEquivalentToSaved(OhtHandoffDefinition setup) => _savedSetup is not null + && Same(_savedSetup.TransportConveyorComponentId, setup.TransportConveyorComponentId) && Same(_savedSetup.RouteAvailableSensorChannelId, setup.RouteAvailableSensorChannelId) + && Same(_savedSetup.VehicleDockedSensorChannelId, setup.VehicleDockedSensorChannelId) && Same(_savedSetup.LoadPortReadySensorChannelId, setup.LoadPortReadySensorChannelId) + && Same(_savedSetup.CarrierReceivedSensorChannelId, setup.CarrierReceivedSensorChannelId) && Same(_savedSetup.HandoffReadyFeedbackChannelId, setup.HandoffReadyFeedbackChannelId) + && Same(_savedSetup.CarrierTransferredFeedbackChannelId, setup.CarrierTransferredFeedbackChannelId); + + private OhtHandoffDraft CaptureDraft() => new(TransportConveyorId, RouteAvailableChannelId, VehicleDockedChannelId, LoadPortReadyChannelId, CarrierReceivedChannelId, HandoffReadyChannelId, CarrierTransferredChannelId); + + private void RestoreDraft(OhtHandoffDraft draft) + { + AddMissing(ConveyorOptions, draft.TransportConveyorId); foreach (var id in new[] { draft.RouteAvailableInputId, draft.VehicleDockedInputId, draft.LoadPortReadyInputId, draft.CarrierReceivedInputId, draft.HandoffReadyInputId, draft.CarrierTransferredInputId }) AddMissing(InputOptions, id); + _transportConveyorId = draft.TransportConveyorId; _routeAvailableChannelId = draft.RouteAvailableInputId; _vehicleDockedChannelId = draft.VehicleDockedInputId; _loadPortReadyChannelId = draft.LoadPortReadyInputId; + _carrierReceivedChannelId = draft.CarrierReceivedInputId; _handoffReadyChannelId = draft.HandoffReadyInputId; _carrierTransferredChannelId = draft.CarrierTransferredInputId; + RaiseValidationChanged(); + } + + private void SetSelection(ref string? field, string? value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private MachineLayoutDefinition? ActiveLayout() => _project is null ? null : ResolveActiveLayout(_project); + private bool IsLayoutComponent(string? id, LayoutComponentKind kind) => ActiveLayout()?.Components.Any(component => component.Kind == kind && Same(component.Id, id)) == true; + private bool IsChannel(string? id, ChannelKind kind) => _project?.Channels.Any(channel => channel.Kind == kind && Same(channel.Id, id)) == true; + + private void RaiseChanged() + { + OnPropertyChanged(nameof(IsVisible)); + RaiseValidationChanged(); + _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private void RaiseValidationChanged() + { + foreach (var property in ValidationProperties) OnPropertyChanged(property); + _applyCommand.RaiseCanExecuteChanged(); + } + + private void RaiseCommandStates() + { + _previewCommand.RaiseCanExecuteChanged(); _applyCommand.RaiseCanExecuteChanged(); _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private static bool Distinct(string?[] values) => values.All(value => !string.IsNullOrWhiteSpace(value)) && values.Distinct(StringComparer.Ordinal).Count() == values.Length; + private static bool Same(string? left, string? right) => string.Equals(left, right, StringComparison.Ordinal); + private static LoadLockSetupOption Option(string id, string? name) => new(id, string.IsNullOrWhiteSpace(name) ? id : $"{name} — {id}"); + private static void AddMissing(ObservableCollection options, string? id) { if (!string.IsNullOrWhiteSpace(id) && options.All(option => !Same(option.Id, id))) options.Add(new LoadLockSetupOption(id, $"{id} ({OpenVisionLanguageService.T("Connections.LoadLockSetupMissing")})")); } + private static MachineLayoutDefinition? ResolveActiveLayout(MachineProjectDocument project) => !string.IsNullOrWhiteSpace(project.Simulation.ActiveLayoutId) ? project.Layouts.FirstOrDefault(layout => Same(layout.Id, project.Simulation.ActiveLayoutId)) : project.Layouts.Count == 1 ? project.Layouts[0] : null; + private static OhtHandoffDefinition Clone(OhtHandoffDefinition value) => new() { TransportConveyorComponentId = value.TransportConveyorComponentId, RouteAvailableSensorChannelId = value.RouteAvailableSensorChannelId, VehicleDockedSensorChannelId = value.VehicleDockedSensorChannelId, LoadPortReadySensorChannelId = value.LoadPortReadySensorChannelId, CarrierReceivedSensorChannelId = value.CarrierReceivedSensorChannelId, HandoffReadyFeedbackChannelId = value.HandoffReadyFeedbackChannelId, CarrierTransferredFeedbackChannelId = value.CarrierTransferredFeedbackChannelId }; + + private sealed record OhtHandoffDraft(string? TransportConveyorId, string? RouteAvailableInputId, string? VehicleDockedInputId, string? LoadPortReadyInputId, string? CarrierReceivedInputId, string? HandoffReadyInputId, string? CarrierTransferredInputId); + private static readonly string[] ValidationProperties = + [ + nameof(TransportConveyorId), nameof(RouteAvailableChannelId), nameof(VehicleDockedChannelId), nameof(LoadPortReadyChannelId), nameof(CarrierReceivedChannelId), nameof(HandoffReadyChannelId), nameof(CarrierTransferredChannelId), nameof(IsTransportConveyorValid), nameof(IsRouteAvailableValid), nameof(IsVehicleDockedValid), nameof(IsLoadPortReadyValid), nameof(IsCarrierReceivedValid), nameof(IsHandoffReadyValid), nameof(IsCarrierTransferredValid), nameof(HasMultipleOhtHandoffs), nameof(HasValidationError), nameof(ValidationText) + ]; +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/PrealignerSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/PrealignerSetupViewModel.cs new file mode 100644 index 0000000..214f843 --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/PrealignerSetupViewModel.cs @@ -0,0 +1,241 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Windows.Input; +using OpenVisionLab; +using OpenVisionLab.Machine.Core.Axes; +using OpenVisionLab.Machine.Core.Channels; +using OpenVisionLab.Machine.Core.Devices; +using OpenVisionLab.Machine.Core.Layouts; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +public sealed class PrealignerSetupViewModel : ViewModelBase +{ + private readonly Func _applySetup; + private readonly Action _clearWorkbenchPreviews; + private readonly RelayCommand _previewCommand; + private readonly RelayCommand _applyCommand; + private readonly RelayCommand _cancelCommand; + private readonly RelayCommand _resetCommand; + private MachineProjectDocument? _project; + private bool _isEditable = true; + private bool _isVisible; + private PrealignerDefinition? _savedSetup; + private string? _rotaryStageComponentId; + private string? _clampCylinderComponentId; + private string? _waferPresentSensorChannelId; + private string? _alignmentAcceptedCommandChannelId; + private string? _alignmentReadyFeedbackChannelId; + private string? _alignmentCompleteFeedbackChannelId; + private string _alignmentTargetText = string.Empty; + private string _alignmentToleranceText = string.Empty; + + public PrealignerSetupViewModel(Func applySetup, Action clearWorkbenchPreviews) + { + _applySetup = applySetup; + _clearWorkbenchPreviews = clearWorkbenchPreviews; + _previewCommand = new RelayCommand(_ => Preview(), _ => IsEditable && _project is not null); + _applyCommand = new RelayCommand(_ => Apply(), ignored => IsEditable && IsVisible && TryCreate(out _)); + _cancelCommand = new RelayCommand(_ => ClearPreviewForCompetingSetup(), _ => IsVisible); + _resetCommand = new RelayCommand(_ => Reset(), _ => IsEditable && IsVisible); + } + + public ObservableCollection StageOptions { get; } = new(); + public ObservableCollection CylinderOptions { get; } = new(); + public ObservableCollection InputOptions { get; } = new(); + public ObservableCollection OutputOptions { get; } = new(); + + public ICommand PreviewCommand => _previewCommand; + public ICommand ApplyCommand => _applyCommand; + public ICommand CancelCommand => _cancelCommand; + public ICommand ResetCommand => _resetCommand; + + public bool IsEditable + { + get => _isEditable; + set + { + if (!SetProperty(ref _isEditable, value)) return; + RaiseCommandStates(); + } + } + + public bool IsVisible => _isVisible; + public string? RotaryStageComponentId { get => _rotaryStageComponentId; set => SetSelection(ref _rotaryStageComponentId, value, nameof(RotaryStageComponentId)); } + public string? ClampCylinderComponentId { get => _clampCylinderComponentId; set => SetSelection(ref _clampCylinderComponentId, value, nameof(ClampCylinderComponentId)); } + public string? WaferPresentSensorChannelId { get => _waferPresentSensorChannelId; set => SetSelection(ref _waferPresentSensorChannelId, value, nameof(WaferPresentSensorChannelId)); } + public string? AlignmentAcceptedCommandChannelId { get => _alignmentAcceptedCommandChannelId; set => SetSelection(ref _alignmentAcceptedCommandChannelId, value, nameof(AlignmentAcceptedCommandChannelId)); } + public string? AlignmentReadyFeedbackChannelId { get => _alignmentReadyFeedbackChannelId; set => SetSelection(ref _alignmentReadyFeedbackChannelId, value, nameof(AlignmentReadyFeedbackChannelId)); } + public string? AlignmentCompleteFeedbackChannelId { get => _alignmentCompleteFeedbackChannelId; set => SetSelection(ref _alignmentCompleteFeedbackChannelId, value, nameof(AlignmentCompleteFeedbackChannelId)); } + public string AlignmentTargetText { get => _alignmentTargetText; set => SetText(ref _alignmentTargetText, value, nameof(AlignmentTargetText)); } + public string AlignmentToleranceText { get => _alignmentToleranceText; set => SetText(ref _alignmentToleranceText, value, nameof(AlignmentToleranceText)); } + public bool HasMultiplePrealigners => _project?.Devices.Count(device => device.Kind == DeviceKind.Prealigner) > 1; + public bool IsRotaryStageValid => TryGetPrealignerStage(RotaryStageComponentId, out _); + public bool IsClampCylinderValid => IsLayoutComponent(ClampCylinderComponentId, LayoutComponentKind.PneumaticCylinder); + public bool IsWaferPresentValid => IsChannel(WaferPresentSensorChannelId, ChannelKind.DigitalInput); + public bool IsAlignmentAcceptedValid => IsChannel(AlignmentAcceptedCommandChannelId, ChannelKind.DigitalOutput); + public bool IsAlignmentReadyValid => IsChannel(AlignmentReadyFeedbackChannelId, ChannelKind.DigitalInput); + public bool IsAlignmentCompleteValid => IsChannel(AlignmentCompleteFeedbackChannelId, ChannelKind.DigitalInput); + public bool IsAlignmentTargetValid => IsRotaryPosition(RotaryStageComponentId, AlignmentTargetText); + public bool IsAlignmentToleranceValid => TryPositiveDouble(AlignmentToleranceText, out _); + public bool HasValidationError => !TryCreate(out _); + public string ValidationText => OpenVisionLanguageService.T( + HasMultiplePrealigners ? "Connections.PrealignerSetupMultipleError" : HasValidationError + ? "Connections.PrealignerSetupValidationError" + : "Connections.PrealignerSetupValidationReady"); + + public void Load(MachineProjectDocument project) + { + _project = project ?? throw new ArgumentNullException(nameof(project)); + ClearPreviewForCompetingSetup(); + RaiseCommandStates(); + } + + internal void ClearPreviewForCompetingSetup() + { + _isVisible = false; + _savedSetup = null; + StageOptions.Clear(); CylinderOptions.Clear(); InputOptions.Clear(); OutputOptions.Clear(); + RaiseChanged(); + } + + internal void RefreshLocalization(Action reloadWorkbench) + { + var draft = IsVisible ? CaptureDraft() : null; + reloadWorkbench(); + if (draft is not null) + { + Preview(); + RestoreDraft(draft); + } + } + + private void Preview() + { + if (_project is null) return; + _clearWorkbenchPreviews(); + StageOptions.Clear(); CylinderOptions.Clear(); InputOptions.Clear(); OutputOptions.Clear(); + var layout = ResolveActiveLayout(_project); + foreach (var component in (layout?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.RotaryStage).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) + StageOptions.Add(Option(component.Id, component.Name)); + foreach (var component in (layout?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.PneumaticCylinder).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) + CylinderOptions.Add(Option(component.Id, component.Name)); + foreach (var channel in _project.Channels.OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) + { + var option = Option(channel.Id, channel.Name); + if (channel.Kind == ChannelKind.DigitalInput) InputOptions.Add(option); + if (channel.Kind == ChannelKind.DigitalOutput) OutputOptions.Add(option); + } + var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Prealigner, Prealigner: not null }).ToArray(); + _savedSetup = existing.Length == 1 ? Clone(existing[0].Prealigner!) : null; + var draft = _savedSetup ?? Suggest(); + AddMissing(StageOptions, draft.RotaryStageComponentId); AddMissing(CylinderOptions, draft.ClampCylinderComponentId); + foreach (var id in new[] { draft.WaferPresentSensorChannelId, draft.AlignmentReadyFeedbackChannelId, draft.AlignmentCompleteFeedbackChannelId }) AddMissing(InputOptions, id); + AddMissing(OutputOptions, draft.AlignmentAcceptedCommandChannelId); + ApplyDraft(draft); + _isVisible = true; + RaiseChanged(); + } + + private void Apply() + { + if (!TryCreate(out var setup)) return; + if (_applySetup(setup) > 0 || IsEquivalentToSaved(setup)) ClearPreviewForCompetingSetup(); else Preview(); + } + + private void Reset() => ApplyDraft(_savedSetup is null ? Suggest() : Clone(_savedSetup)); + + private PrealignerDefinition Suggest() => new() + { + RotaryStageComponentId = StageOptions.FirstOrDefault()?.Id ?? string.Empty, + ClampCylinderComponentId = CylinderOptions.FirstOrDefault()?.Id ?? string.Empty, + WaferPresentSensorChannelId = InputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + AlignmentReadyFeedbackChannelId = InputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + AlignmentCompleteFeedbackChannelId = InputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, + AlignmentAcceptedCommandChannelId = OutputOptions.FirstOrDefault()?.Id ?? string.Empty, + AlignmentTargetDegrees = StageAxisLimit(StageOptions.FirstOrDefault()?.Id), + AlignmentToleranceDegrees = 0.1 + }; + + private void ApplyDraft(PrealignerDefinition setup) + { + _rotaryStageComponentId = setup.RotaryStageComponentId; _clampCylinderComponentId = setup.ClampCylinderComponentId; _waferPresentSensorChannelId = setup.WaferPresentSensorChannelId; + _alignmentAcceptedCommandChannelId = setup.AlignmentAcceptedCommandChannelId; _alignmentReadyFeedbackChannelId = setup.AlignmentReadyFeedbackChannelId; _alignmentCompleteFeedbackChannelId = setup.AlignmentCompleteFeedbackChannelId; + _alignmentTargetText = Format(setup.AlignmentTargetDegrees); _alignmentToleranceText = Format(setup.AlignmentToleranceDegrees); + RaiseValidationChanged(); + } + + private bool TryCreate(out PrealignerDefinition setup) + { + var channels = new[] { WaferPresentSensorChannelId, AlignmentAcceptedCommandChannelId, AlignmentReadyFeedbackChannelId, AlignmentCompleteFeedbackChannelId }; + setup = new PrealignerDefinition + { + RotaryStageComponentId = RotaryStageComponentId ?? string.Empty, ClampCylinderComponentId = ClampCylinderComponentId ?? string.Empty, + WaferPresentSensorChannelId = WaferPresentSensorChannelId ?? string.Empty, AlignmentAcceptedCommandChannelId = AlignmentAcceptedCommandChannelId ?? string.Empty, + AlignmentReadyFeedbackChannelId = AlignmentReadyFeedbackChannelId ?? string.Empty, AlignmentCompleteFeedbackChannelId = AlignmentCompleteFeedbackChannelId ?? string.Empty, + AlignmentTargetDegrees = Parse(AlignmentTargetText), AlignmentToleranceDegrees = Parse(AlignmentToleranceText) + }; + return !HasMultiplePrealigners && IsRotaryStageValid && IsClampCylinderValid && Distinct(channels) + && IsWaferPresentValid && IsAlignmentAcceptedValid && IsAlignmentReadyValid && IsAlignmentCompleteValid && IsAlignmentTargetValid && IsAlignmentToleranceValid; + } + + private bool IsEquivalentToSaved(PrealignerDefinition setup) => _savedSetup is not null + && Same(_savedSetup.RotaryStageComponentId, setup.RotaryStageComponentId) && Same(_savedSetup.ClampCylinderComponentId, setup.ClampCylinderComponentId) + && Same(_savedSetup.WaferPresentSensorChannelId, setup.WaferPresentSensorChannelId) && Same(_savedSetup.AlignmentAcceptedCommandChannelId, setup.AlignmentAcceptedCommandChannelId) + && Same(_savedSetup.AlignmentReadyFeedbackChannelId, setup.AlignmentReadyFeedbackChannelId) && Same(_savedSetup.AlignmentCompleteFeedbackChannelId, setup.AlignmentCompleteFeedbackChannelId) + && _savedSetup.AlignmentTargetDegrees == setup.AlignmentTargetDegrees && _savedSetup.AlignmentToleranceDegrees == setup.AlignmentToleranceDegrees; + + private PrealignerDraft CaptureDraft() => new(RotaryStageComponentId, ClampCylinderComponentId, WaferPresentSensorChannelId, AlignmentAcceptedCommandChannelId, AlignmentReadyFeedbackChannelId, AlignmentCompleteFeedbackChannelId, AlignmentTargetText, AlignmentToleranceText); + + private void RestoreDraft(PrealignerDraft draft) + { + _rotaryStageComponentId = draft.StageId; _clampCylinderComponentId = draft.ClampId; _waferPresentSensorChannelId = draft.WaferPresentId; _alignmentAcceptedCommandChannelId = draft.AcceptedId; + _alignmentReadyFeedbackChannelId = draft.ReadyId; _alignmentCompleteFeedbackChannelId = draft.CompleteId; _alignmentTargetText = draft.Target; _alignmentToleranceText = draft.Tolerance; + RaiseValidationChanged(); + } + + private void SetSelection(ref string? field, string? value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private void SetText(ref string field, string value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private MachineLayoutDefinition? ActiveLayout() => _project is null ? null : ResolveActiveLayout(_project); + private bool IsLayoutComponent(string? id, LayoutComponentKind kind) => ActiveLayout()?.Components.Any(component => component.Kind == kind && Same(component.Id, id)) == true; + private bool IsChannel(string? id, ChannelKind kind) => _project?.Channels.Any(channel => channel.Kind == kind && Same(channel.Id, id)) == true; + private bool IsRotaryPosition(string? stageId, string text) => TryGetPrealignerStage(stageId, out var stage) && TryFiniteDouble(text, out var value) && _project!.Axes.FirstOrDefault(axis => Same(axis.Id, stage.BehaviorBindingId)) is { Kind: AxisKind.Rotary, SoftLimitMin: not null, SoftLimitMax: not null } axis && value >= axis.SoftLimitMin.Value && value <= axis.SoftLimitMax.Value; + private bool TryGetPrealignerStage(string? id, out LayoutComponentDefinition stage) { var value = ActiveLayout()?.Components.FirstOrDefault(component => component.Kind == LayoutComponentKind.RotaryStage && Same(component.Id, id)); stage = value!; return value is not null && _project!.Axes.Any(axis => axis.Kind == AxisKind.Rotary && Same(axis.Id, value.BehaviorBindingId)); } + private double StageAxisLimit(string? stageId) => TryGetPrealignerStage(stageId, out var stage) && _project!.Axes.FirstOrDefault(axis => Same(axis.Id, stage.BehaviorBindingId)) is { SoftLimitMin: not null } axis ? axis.SoftLimitMin.Value : 0; + + private void RaiseChanged() + { + OnPropertyChanged(nameof(IsVisible)); + RaiseValidationChanged(); + _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private void RaiseValidationChanged() + { + foreach (var property in ValidationProperties) OnPropertyChanged(property); + _applyCommand.RaiseCanExecuteChanged(); + } + + private void RaiseCommandStates() + { + _previewCommand.RaiseCanExecuteChanged(); _applyCommand.RaiseCanExecuteChanged(); _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private static bool Distinct(string?[] values) => values.All(value => !string.IsNullOrWhiteSpace(value)) && values.Distinct(StringComparer.Ordinal).Count() == values.Length; + private static bool Same(string? left, string? right) => string.Equals(left, right, StringComparison.Ordinal); + private static bool TryPositiveDouble(string text, out double value) => TryFiniteDouble(text, out value) && value > 0; + private static bool TryFiniteDouble(string text, out double value) => (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) || double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) && double.IsFinite(value); + private static double Parse(string text) => TryFiniteDouble(text, out var value) ? value : double.NaN; + private static string Format(double value) => value.ToString("0.###", CultureInfo.CurrentCulture); + private static LoadLockSetupOption Option(string id, string? name) => new(id, string.IsNullOrWhiteSpace(name) ? id : $"{name} — {id}"); + private static void AddMissing(ObservableCollection options, string? id) { if (!string.IsNullOrWhiteSpace(id) && options.All(option => !Same(option.Id, id))) options.Add(new LoadLockSetupOption(id, $"{id} ({OpenVisionLanguageService.T("Connections.LoadLockSetupMissing")})")); } + private static MachineLayoutDefinition? ResolveActiveLayout(MachineProjectDocument project) => !string.IsNullOrWhiteSpace(project.Simulation.ActiveLayoutId) ? project.Layouts.FirstOrDefault(layout => Same(layout.Id, project.Simulation.ActiveLayoutId)) : project.Layouts.Count == 1 ? project.Layouts[0] : null; + private static PrealignerDefinition Clone(PrealignerDefinition value) => new() { RotaryStageComponentId = value.RotaryStageComponentId, ClampCylinderComponentId = value.ClampCylinderComponentId, WaferPresentSensorChannelId = value.WaferPresentSensorChannelId, AlignmentAcceptedCommandChannelId = value.AlignmentAcceptedCommandChannelId, AlignmentReadyFeedbackChannelId = value.AlignmentReadyFeedbackChannelId, AlignmentCompleteFeedbackChannelId = value.AlignmentCompleteFeedbackChannelId, AlignmentTargetDegrees = value.AlignmentTargetDegrees, AlignmentToleranceDegrees = value.AlignmentToleranceDegrees }; + + private sealed record PrealignerDraft(string? StageId, string? ClampId, string? WaferPresentId, string? AcceptedId, string? ReadyId, string? CompleteId, string Target, string Tolerance); + private static readonly string[] ValidationProperties = + [ + nameof(RotaryStageComponentId), nameof(ClampCylinderComponentId), nameof(WaferPresentSensorChannelId), nameof(AlignmentAcceptedCommandChannelId), nameof(AlignmentReadyFeedbackChannelId), nameof(AlignmentCompleteFeedbackChannelId), nameof(AlignmentTargetText), nameof(AlignmentToleranceText), nameof(IsRotaryStageValid), nameof(IsClampCylinderValid), nameof(IsWaferPresentValid), nameof(IsAlignmentAcceptedValid), nameof(IsAlignmentReadyValid), nameof(IsAlignmentCompleteValid), nameof(IsAlignmentTargetValid), nameof(IsAlignmentToleranceValid), nameof(HasMultiplePrealigners), nameof(HasValidationError), nameof(ValidationText) + ]; +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/SemanticEquipmentSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/SemanticEquipmentSetupViewModel.cs new file mode 100644 index 0000000..8882d84 --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/SemanticEquipmentSetupViewModel.cs @@ -0,0 +1,94 @@ +using OpenVisionLab.Machine.Core.Devices; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +/// +/// Composes the semantic setup cards and coordinates their shared preview lifetime. +/// Each child owns one equipment definition and its editable draft. +/// +public sealed class SemanticEquipmentSetupViewModel : ViewModelBase +{ + private bool _isEditable = true; + + public SemanticEquipmentSetupViewModel( + Func applyWaferHandlerSetup, + Func applyPrealignerSetup, + Func applyInspectionHandoffSetup, + Func applyInspectionSortRouterSetup, + Func applyOhtHandoffSetup, + Action clearWorkbenchPreviews) + { + WaferHandler = new WaferHandlerSetupViewModel(applyWaferHandlerSetup, clearWorkbenchPreviews); + Prealigner = new PrealignerSetupViewModel(applyPrealignerSetup, clearWorkbenchPreviews); + InspectionHandoff = new InspectionHandoffSetupViewModel(applyInspectionHandoffSetup, clearWorkbenchPreviews); + InspectionSortRouter = new InspectionSortRouterSetupViewModel(applyInspectionSortRouterSetup, clearWorkbenchPreviews); + OhtHandoff = new OhtHandoffSetupViewModel(applyOhtHandoffSetup, clearWorkbenchPreviews); + } + + public WaferHandlerSetupViewModel WaferHandler { get; } + public PrealignerSetupViewModel Prealigner { get; } + public InspectionHandoffSetupViewModel InspectionHandoff { get; } + public InspectionSortRouterSetupViewModel InspectionSortRouter { get; } + public OhtHandoffSetupViewModel OhtHandoff { get; } + + public bool IsEditable + { + get => _isEditable; + set + { + if (!SetProperty(ref _isEditable, value)) return; + WaferHandler.IsEditable = value; + Prealigner.IsEditable = value; + InspectionHandoff.IsEditable = value; + InspectionSortRouter.IsEditable = value; + OhtHandoff.IsEditable = value; + } + } + + public void Load(MachineProjectDocument project) + { + WaferHandler.Load(project); + Prealigner.Load(project); + InspectionHandoff.Load(project); + InspectionSortRouter.Load(project); + OhtHandoff.Load(project); + } + + public void ClearPreviewForCompetingSetup() + { + WaferHandler.ClearPreviewForCompetingSetup(); + Prealigner.ClearPreviewForCompetingSetup(); + InspectionHandoff.ClearPreviewForCompetingSetup(); + InspectionSortRouter.ClearPreviewForCompetingSetup(); + OhtHandoff.ClearPreviewForCompetingSetup(); + } + + internal void RefreshLocalization(Action reloadWorkbench) + { + if (WaferHandler.IsVisible) + { + WaferHandler.RefreshLocalization(reloadWorkbench); + } + else if (Prealigner.IsVisible) + { + Prealigner.RefreshLocalization(reloadWorkbench); + } + else if (InspectionHandoff.IsVisible) + { + InspectionHandoff.RefreshLocalization(reloadWorkbench); + } + else if (InspectionSortRouter.IsVisible) + { + InspectionSortRouter.RefreshLocalization(reloadWorkbench); + } + else if (OhtHandoff.IsVisible) + { + OhtHandoff.RefreshLocalization(reloadWorkbench); + } + else + { + reloadWorkbench(); + } + } +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/WaferHandlerSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/WaferHandlerSetupViewModel.cs new file mode 100644 index 0000000..fbec91f --- /dev/null +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Semantic/WaferHandlerSetupViewModel.cs @@ -0,0 +1,270 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Windows.Input; +using OpenVisionLab; +using OpenVisionLab.Machine.Core.Axes; +using OpenVisionLab.Machine.Core.Channels; +using OpenVisionLab.Machine.Core.Devices; +using OpenVisionLab.Machine.Core.Layouts; +using OpenVisionLab.Machine.Core.Projects; + +namespace OpenVisionLab.MachineStudio.ViewModel; + +public sealed class WaferHandlerSetupViewModel : ViewModelBase +{ + private readonly Func _applySetup; + private readonly Action _clearWorkbenchPreviews; + private readonly RelayCommand _previewCommand; + private readonly RelayCommand _applyCommand; + private readonly RelayCommand _cancelCommand; + private readonly RelayCommand _resetCommand; + private MachineProjectDocument? _project; + private bool _isEditable = true; + private bool _isVisible; + private WaferHandlerDefinition? _savedSetup; + private string? _horizontalAxisId; + private string? _verticalAxisId; + private string? _workpieceComponentId; + private string? _sourcePresentSensorChannelId; + private string? _gateOpenSensorChannelId; + private string? _pickCommandChannelId; + private string? _placeCommandChannelId; + private string? _holdingFeedbackChannelId; + private string? _placedFeedbackChannelId; + private string _pickHorizontalText = string.Empty; + private string _pickVerticalText = string.Empty; + private string _placeHorizontalText = string.Empty; + private string _placeVerticalText = string.Empty; + + public WaferHandlerSetupViewModel(Func applySetup, Action clearWorkbenchPreviews) + { + _applySetup = applySetup; + _clearWorkbenchPreviews = clearWorkbenchPreviews; + _previewCommand = new RelayCommand(_ => Preview(), _ => IsEditable && _project is not null); + _applyCommand = new RelayCommand(_ => Apply(), ignored => IsEditable && IsVisible && TryCreate(out _)); + _cancelCommand = new RelayCommand(_ => ClearPreviewForCompetingSetup(), _ => IsVisible); + _resetCommand = new RelayCommand(_ => Reset(), _ => IsEditable && IsVisible); + } + + public ObservableCollection AxisOptions { get; } = new(); + public ObservableCollection WorkpieceOptions { get; } = new(); + public ObservableCollection InputOptions { get; } = new(); + public ObservableCollection OutputOptions { get; } = new(); + + public ICommand PreviewCommand => _previewCommand; + public ICommand ApplyCommand => _applyCommand; + public ICommand CancelCommand => _cancelCommand; + public ICommand ResetCommand => _resetCommand; + + public bool IsEditable + { + get => _isEditable; + set + { + if (!SetProperty(ref _isEditable, value)) return; + RaiseCommandStates(); + } + } + + public bool IsVisible => _isVisible; + public string? HorizontalAxisId { get => _horizontalAxisId; set => SetSelection(ref _horizontalAxisId, value, nameof(HorizontalAxisId)); } + public string? VerticalAxisId { get => _verticalAxisId; set => SetSelection(ref _verticalAxisId, value, nameof(VerticalAxisId)); } + public string? WorkpieceComponentId { get => _workpieceComponentId; set => SetSelection(ref _workpieceComponentId, value, nameof(WorkpieceComponentId)); } + public string? SourcePresentSensorChannelId { get => _sourcePresentSensorChannelId; set => SetSelection(ref _sourcePresentSensorChannelId, value, nameof(SourcePresentSensorChannelId)); } + public string? GateOpenSensorChannelId { get => _gateOpenSensorChannelId; set => SetSelection(ref _gateOpenSensorChannelId, value, nameof(GateOpenSensorChannelId)); } + public string? PickCommandChannelId { get => _pickCommandChannelId; set => SetSelection(ref _pickCommandChannelId, value, nameof(PickCommandChannelId)); } + public string? PlaceCommandChannelId { get => _placeCommandChannelId; set => SetSelection(ref _placeCommandChannelId, value, nameof(PlaceCommandChannelId)); } + public string? HoldingFeedbackChannelId { get => _holdingFeedbackChannelId; set => SetSelection(ref _holdingFeedbackChannelId, value, nameof(HoldingFeedbackChannelId)); } + public string? PlacedFeedbackChannelId { get => _placedFeedbackChannelId; set => SetSelection(ref _placedFeedbackChannelId, value, nameof(PlacedFeedbackChannelId)); } + public string PickHorizontalText { get => _pickHorizontalText; set => SetText(ref _pickHorizontalText, value, nameof(PickHorizontalText)); } + public string PickVerticalText { get => _pickVerticalText; set => SetText(ref _pickVerticalText, value, nameof(PickVerticalText)); } + public string PlaceHorizontalText { get => _placeHorizontalText; set => SetText(ref _placeHorizontalText, value, nameof(PlaceHorizontalText)); } + public string PlaceVerticalText { get => _placeVerticalText; set => SetText(ref _placeVerticalText, value, nameof(PlaceVerticalText)); } + public bool HasMultipleHandlers => _project?.Devices.Count(device => device.Kind == DeviceKind.Handler) > 1; + public bool IsHorizontalAxisValid => IsLinearAxis(HorizontalAxisId); + public bool IsVerticalAxisValid => IsLinearAxis(VerticalAxisId) && !Same(HorizontalAxisId, VerticalAxisId); + public bool IsWorkpieceValid => IsLayoutComponent(WorkpieceComponentId, LayoutComponentKind.Workpiece); + public bool IsSourcePresentValid => IsChannel(SourcePresentSensorChannelId, ChannelKind.DigitalInput); + public bool IsGateOpenValid => IsChannel(GateOpenSensorChannelId, ChannelKind.DigitalInput); + public bool IsPickCommandValid => IsChannel(PickCommandChannelId, ChannelKind.DigitalOutput); + public bool IsPlaceCommandValid => IsChannel(PlaceCommandChannelId, ChannelKind.DigitalOutput); + public bool IsHoldingFeedbackValid => IsChannel(HoldingFeedbackChannelId, ChannelKind.DigitalInput); + public bool IsPlacedFeedbackValid => IsChannel(PlacedFeedbackChannelId, ChannelKind.DigitalInput); + public bool IsPickHorizontalValid => IsAxisPosition(HorizontalAxisId, PickHorizontalText); + public bool IsPickVerticalValid => IsAxisPosition(VerticalAxisId, PickVerticalText); + public bool IsPlaceHorizontalValid => IsAxisPosition(HorizontalAxisId, PlaceHorizontalText); + public bool IsPlaceVerticalValid => IsAxisPosition(VerticalAxisId, PlaceVerticalText); + public bool HasValidationError => !TryCreate(out _); + public string ValidationText => OpenVisionLanguageService.T( + HasMultipleHandlers ? "Connections.WaferHandlerSetupMultipleError" : HasValidationError + ? "Connections.WaferHandlerSetupValidationError" + : "Connections.WaferHandlerSetupValidationReady"); + + public void Load(MachineProjectDocument project) + { + _project = project ?? throw new ArgumentNullException(nameof(project)); + ClearPreviewForCompetingSetup(); + RaiseCommandStates(); + } + + internal void ClearPreviewForCompetingSetup() + { + _isVisible = false; + _savedSetup = null; + AxisOptions.Clear(); WorkpieceOptions.Clear(); InputOptions.Clear(); OutputOptions.Clear(); + RaiseChanged(); + } + + internal void RefreshLocalization(Action reloadWorkbench) + { + var draft = IsVisible ? CaptureDraft() : null; + reloadWorkbench(); + if (draft is not null) + { + Preview(); + RestoreDraft(draft); + } + } + + private void Preview() + { + if (_project is null) return; + _clearWorkbenchPreviews(); + AxisOptions.Clear(); WorkpieceOptions.Clear(); InputOptions.Clear(); OutputOptions.Clear(); + var layout = ResolveActiveLayout(_project); + foreach (var axis in _project.Axes.Where(axis => axis.Kind == AxisKind.Linear).OrderBy(axis => axis.Name, StringComparer.CurrentCulture).ThenBy(axis => axis.Id, StringComparer.Ordinal)) + AxisOptions.Add(Option(axis.Id, axis.Name)); + foreach (var component in (layout?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.Workpiece).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) + WorkpieceOptions.Add(Option(component.Id, component.Name)); + foreach (var channel in _project.Channels.OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) + { + var option = Option(channel.Id, channel.Name); + if (channel.Kind == ChannelKind.DigitalInput) InputOptions.Add(option); + if (channel.Kind == ChannelKind.DigitalOutput) OutputOptions.Add(option); + } + var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Handler, WaferHandler: not null }).ToArray(); + _savedSetup = existing.Length == 1 ? Clone(existing[0].WaferHandler!) : null; + var draft = _savedSetup ?? Suggest(); + AddMissing(AxisOptions, draft.HorizontalAxisId); AddMissing(AxisOptions, draft.VerticalAxisId); AddMissing(WorkpieceOptions, draft.WorkpieceComponentId); + foreach (var id in new[] { draft.SourcePresentSensorChannelId, draft.GateOpenSensorChannelId, draft.HoldingFeedbackChannelId, draft.PlacedFeedbackChannelId }) AddMissing(InputOptions, id); + foreach (var id in new[] { draft.PickCommandChannelId, draft.PlaceCommandChannelId }) AddMissing(OutputOptions, id); + ApplyDraft(draft); + _isVisible = true; + RaiseChanged(); + } + + private void Apply() + { + if (!TryCreate(out var setup)) return; + if (_applySetup(setup) > 0 || IsEquivalentToSaved(setup)) ClearPreviewForCompetingSetup(); else Preview(); + } + + private void Reset() => ApplyDraft(_savedSetup is null ? Suggest() : Clone(_savedSetup)); + + private WaferHandlerDefinition Suggest() => new() + { + HorizontalAxisId = AxisOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + VerticalAxisId = AxisOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + WorkpieceComponentId = WorkpieceOptions.FirstOrDefault()?.Id ?? string.Empty, + SourcePresentSensorChannelId = InputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + GateOpenSensorChannelId = InputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + HoldingFeedbackChannelId = InputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, + PlacedFeedbackChannelId = InputOptions.ElementAtOrDefault(3)?.Id ?? string.Empty, + PickCommandChannelId = OutputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, + PlaceCommandChannelId = OutputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, + PickHorizontalPosition = AxisLimit(AxisOptions.ElementAtOrDefault(0)?.Id, false), + PickVerticalPosition = AxisLimit(AxisOptions.ElementAtOrDefault(1)?.Id, false), + PlaceHorizontalPosition = AxisLimit(AxisOptions.ElementAtOrDefault(0)?.Id, true), + PlaceVerticalPosition = AxisLimit(AxisOptions.ElementAtOrDefault(1)?.Id, true) + }; + + private void ApplyDraft(WaferHandlerDefinition setup) + { + _horizontalAxisId = setup.HorizontalAxisId; _verticalAxisId = setup.VerticalAxisId; _workpieceComponentId = setup.WorkpieceComponentId; + _sourcePresentSensorChannelId = setup.SourcePresentSensorChannelId; _gateOpenSensorChannelId = setup.GateOpenSensorChannelId; + _pickCommandChannelId = setup.PickCommandChannelId; _placeCommandChannelId = setup.PlaceCommandChannelId; + _holdingFeedbackChannelId = setup.HoldingFeedbackChannelId; _placedFeedbackChannelId = setup.PlacedFeedbackChannelId; + _pickHorizontalText = Format(setup.PickHorizontalPosition); _pickVerticalText = Format(setup.PickVerticalPosition); + _placeHorizontalText = Format(setup.PlaceHorizontalPosition); _placeVerticalText = Format(setup.PlaceVerticalPosition); + RaiseValidationChanged(); + } + + private bool TryCreate(out WaferHandlerDefinition setup) + { + var channels = new[] { SourcePresentSensorChannelId, GateOpenSensorChannelId, PickCommandChannelId, PlaceCommandChannelId, HoldingFeedbackChannelId, PlacedFeedbackChannelId }; + setup = new WaferHandlerDefinition + { + HorizontalAxisId = HorizontalAxisId ?? string.Empty, VerticalAxisId = VerticalAxisId ?? string.Empty, WorkpieceComponentId = WorkpieceComponentId ?? string.Empty, + SourcePresentSensorChannelId = SourcePresentSensorChannelId ?? string.Empty, GateOpenSensorChannelId = GateOpenSensorChannelId ?? string.Empty, + PickCommandChannelId = PickCommandChannelId ?? string.Empty, PlaceCommandChannelId = PlaceCommandChannelId ?? string.Empty, + HoldingFeedbackChannelId = HoldingFeedbackChannelId ?? string.Empty, PlacedFeedbackChannelId = PlacedFeedbackChannelId ?? string.Empty, + PickHorizontalPosition = Parse(PickHorizontalText), PickVerticalPosition = Parse(PickVerticalText), PlaceHorizontalPosition = Parse(PlaceHorizontalText), PlaceVerticalPosition = Parse(PlaceVerticalText) + }; + return !HasMultipleHandlers && IsHorizontalAxisValid && IsVerticalAxisValid && IsWorkpieceValid && Distinct(channels) + && IsSourcePresentValid && IsGateOpenValid && IsPickCommandValid && IsPlaceCommandValid && IsHoldingFeedbackValid && IsPlacedFeedbackValid + && IsPickHorizontalValid && IsPickVerticalValid && IsPlaceHorizontalValid && IsPlaceVerticalValid; + } + + private bool IsEquivalentToSaved(WaferHandlerDefinition setup) => _savedSetup is not null + && Same(_savedSetup.HorizontalAxisId, setup.HorizontalAxisId) && Same(_savedSetup.VerticalAxisId, setup.VerticalAxisId) && Same(_savedSetup.WorkpieceComponentId, setup.WorkpieceComponentId) + && Same(_savedSetup.SourcePresentSensorChannelId, setup.SourcePresentSensorChannelId) && Same(_savedSetup.GateOpenSensorChannelId, setup.GateOpenSensorChannelId) + && Same(_savedSetup.PickCommandChannelId, setup.PickCommandChannelId) && Same(_savedSetup.PlaceCommandChannelId, setup.PlaceCommandChannelId) + && Same(_savedSetup.HoldingFeedbackChannelId, setup.HoldingFeedbackChannelId) && Same(_savedSetup.PlacedFeedbackChannelId, setup.PlacedFeedbackChannelId) + && _savedSetup.PickHorizontalPosition == setup.PickHorizontalPosition && _savedSetup.PickVerticalPosition == setup.PickVerticalPosition + && _savedSetup.PlaceHorizontalPosition == setup.PlaceHorizontalPosition && _savedSetup.PlaceVerticalPosition == setup.PlaceVerticalPosition; + + private WaferHandlerDraft CaptureDraft() => new(HorizontalAxisId, VerticalAxisId, WorkpieceComponentId, SourcePresentSensorChannelId, GateOpenSensorChannelId, PickCommandChannelId, PlaceCommandChannelId, HoldingFeedbackChannelId, PlacedFeedbackChannelId, PickHorizontalText, PickVerticalText, PlaceHorizontalText, PlaceVerticalText); + + private void RestoreDraft(WaferHandlerDraft draft) + { + _horizontalAxisId = draft.HorizontalAxisId; _verticalAxisId = draft.VerticalAxisId; _workpieceComponentId = draft.WorkpieceComponentId; + _sourcePresentSensorChannelId = draft.SourceInputId; _gateOpenSensorChannelId = draft.GateInputId; _pickCommandChannelId = draft.PickOutputId; _placeCommandChannelId = draft.PlaceOutputId; + _holdingFeedbackChannelId = draft.HoldingInputId; _placedFeedbackChannelId = draft.PlacedInputId; _pickHorizontalText = draft.PickHorizontal; _pickVerticalText = draft.PickVertical; + _placeHorizontalText = draft.PlaceHorizontal; _placeVerticalText = draft.PlaceVertical; + RaiseValidationChanged(); + } + + private void SetSelection(ref string? field, string? value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private void SetText(ref string field, string value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } + private MachineLayoutDefinition? ActiveLayout() => _project is null ? null : ResolveActiveLayout(_project); + private bool IsLinearAxis(string? id) => _project?.Axes.Any(axis => axis.Kind == AxisKind.Linear && Same(axis.Id, id)) == true; + private bool IsLayoutComponent(string? id, LayoutComponentKind kind) => ActiveLayout()?.Components.Any(component => component.Kind == kind && Same(component.Id, id)) == true; + private bool IsChannel(string? id, ChannelKind kind) => _project?.Channels.Any(channel => channel.Kind == kind && Same(channel.Id, id)) == true; + private bool IsAxisPosition(string? axisId, string text) => TryGetAxis(axisId, out var axis) && TryFiniteDouble(text, out var value) && axis.SoftLimitMin.HasValue && axis.SoftLimitMax.HasValue && value >= axis.SoftLimitMin.Value && value <= axis.SoftLimitMax.Value; + private bool TryGetAxis(string? id, out VirtualAxisDefinition axis) { axis = _project?.Axes.FirstOrDefault(candidate => Same(candidate.Id, id))!; return axis is not null; } + private double AxisLimit(string? axisId, bool maximum) => TryGetAxis(axisId, out var axis) && axis.SoftLimitMin.HasValue && axis.SoftLimitMax.HasValue ? maximum ? axis.SoftLimitMax.Value : axis.SoftLimitMin.Value : 0; + + private void RaiseChanged() + { + OnPropertyChanged(nameof(IsVisible)); + RaiseValidationChanged(); + _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private void RaiseValidationChanged() + { + foreach (var property in ValidationProperties) OnPropertyChanged(property); + _applyCommand.RaiseCanExecuteChanged(); + } + + private void RaiseCommandStates() + { + _previewCommand.RaiseCanExecuteChanged(); _applyCommand.RaiseCanExecuteChanged(); _cancelCommand.RaiseCanExecuteChanged(); _resetCommand.RaiseCanExecuteChanged(); + } + + private static bool Distinct(string?[] values) => values.All(value => !string.IsNullOrWhiteSpace(value)) && values.Distinct(StringComparer.Ordinal).Count() == values.Length; + private static bool Same(string? left, string? right) => string.Equals(left, right, StringComparison.Ordinal); + private static bool TryFiniteDouble(string text, out double value) => (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) || double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) && double.IsFinite(value); + private static double Parse(string text) => TryFiniteDouble(text, out var value) ? value : double.NaN; + private static string Format(double value) => value.ToString("0.###", CultureInfo.CurrentCulture); + private static LoadLockSetupOption Option(string id, string? name) => new(id, string.IsNullOrWhiteSpace(name) ? id : $"{name} — {id}"); + private static void AddMissing(ObservableCollection options, string? id) { if (!string.IsNullOrWhiteSpace(id) && options.All(option => !Same(option.Id, id))) options.Add(new LoadLockSetupOption(id, $"{id} ({OpenVisionLanguageService.T("Connections.LoadLockSetupMissing")})")); } + private static MachineLayoutDefinition? ResolveActiveLayout(MachineProjectDocument project) => !string.IsNullOrWhiteSpace(project.Simulation.ActiveLayoutId) ? project.Layouts.FirstOrDefault(layout => Same(layout.Id, project.Simulation.ActiveLayoutId)) : project.Layouts.Count == 1 ? project.Layouts[0] : null; + private static WaferHandlerDefinition Clone(WaferHandlerDefinition value) => new() { HorizontalAxisId = value.HorizontalAxisId, VerticalAxisId = value.VerticalAxisId, WorkpieceComponentId = value.WorkpieceComponentId, SourcePresentSensorChannelId = value.SourcePresentSensorChannelId, GateOpenSensorChannelId = value.GateOpenSensorChannelId, PickCommandChannelId = value.PickCommandChannelId, PlaceCommandChannelId = value.PlaceCommandChannelId, HoldingFeedbackChannelId = value.HoldingFeedbackChannelId, PlacedFeedbackChannelId = value.PlacedFeedbackChannelId, PickHorizontalPosition = value.PickHorizontalPosition, PickVerticalPosition = value.PickVerticalPosition, PlaceHorizontalPosition = value.PlaceHorizontalPosition, PlaceVerticalPosition = value.PlaceVerticalPosition }; + + private sealed record WaferHandlerDraft(string? HorizontalAxisId, string? VerticalAxisId, string? WorkpieceComponentId, string? SourceInputId, string? GateInputId, string? PickOutputId, string? PlaceOutputId, string? HoldingInputId, string? PlacedInputId, string PickHorizontal, string PickVertical, string PlaceHorizontal, string PlaceVertical); + private static readonly string[] ValidationProperties = + [ + nameof(HorizontalAxisId), nameof(VerticalAxisId), nameof(WorkpieceComponentId), nameof(SourcePresentSensorChannelId), nameof(GateOpenSensorChannelId), nameof(PickCommandChannelId), nameof(PlaceCommandChannelId), nameof(HoldingFeedbackChannelId), nameof(PlacedFeedbackChannelId), nameof(PickHorizontalText), nameof(PickVerticalText), nameof(PlaceHorizontalText), nameof(PlaceVerticalText), nameof(IsHorizontalAxisValid), nameof(IsVerticalAxisValid), nameof(IsWorkpieceValid), nameof(IsSourcePresentValid), nameof(IsGateOpenValid), nameof(IsPickCommandValid), nameof(IsPlaceCommandValid), nameof(IsHoldingFeedbackValid), nameof(IsPlacedFeedbackValid), nameof(IsPickHorizontalValid), nameof(IsPickVerticalValid), nameof(IsPlaceHorizontalValid), nameof(IsPlaceVerticalValid), nameof(HasMultipleHandlers), nameof(HasValidationError), nameof(ValidationText) + ]; +} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemanticEquipmentSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemanticEquipmentSetupViewModel.cs deleted file mode 100644 index 54ec136..0000000 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/SemanticEquipmentSetupViewModel.cs +++ /dev/null @@ -1,541 +0,0 @@ -using System.Collections.ObjectModel; -using System.Globalization; -using System.Windows.Input; -using OpenVisionLab; -using OpenVisionLab.Machine.Core.Axes; -using OpenVisionLab.Machine.Core.Channels; -using OpenVisionLab.Machine.Core.Devices; -using OpenVisionLab.Machine.Core.Layouts; -using OpenVisionLab.Machine.Core.Projects; - -namespace OpenVisionLab.MachineStudio.ViewModel; - -public sealed class SemanticEquipmentSetupViewModel : ViewModelBase -{ - private readonly Func _applyWaferHandlerSetup; - private readonly Func _applyPrealignerSetup; - private readonly Func _applyInspectionHandoffSetup; - private readonly Func _applyInspectionSortRouterSetup; - private readonly Func _applyOhtHandoffSetup; - private readonly Action _clearWorkbenchPreviews; - private readonly RelayCommand _previewWaferHandlerSetupCommand; - private readonly RelayCommand _applyWaferHandlerSetupCommand; - private readonly RelayCommand _cancelWaferHandlerSetupCommand; - private readonly RelayCommand _resetWaferHandlerSetupCommand; - private readonly RelayCommand _previewPrealignerSetupCommand; - private readonly RelayCommand _applyPrealignerSetupCommand; - private readonly RelayCommand _cancelPrealignerSetupCommand; - private readonly RelayCommand _resetPrealignerSetupCommand; - private readonly RelayCommand _previewInspectionHandoffSetupCommand; - private readonly RelayCommand _applyInspectionHandoffSetupCommand; - private readonly RelayCommand _cancelInspectionHandoffSetupCommand; - private readonly RelayCommand _resetInspectionHandoffSetupCommand; - private readonly RelayCommand _previewInspectionSortRouterSetupCommand; - private readonly RelayCommand _applyInspectionSortRouterSetupCommand; - private readonly RelayCommand _cancelInspectionSortRouterSetupCommand; - private readonly RelayCommand _resetInspectionSortRouterSetupCommand; - private readonly RelayCommand _previewOhtHandoffSetupCommand; - private readonly RelayCommand _applyOhtHandoffSetupCommand; - private readonly RelayCommand _cancelOhtHandoffSetupCommand; - private readonly RelayCommand _resetOhtHandoffSetupCommand; - private MachineProjectDocument? _project; - private bool _isEditable = true; - private bool _isWaferHandlerSetupVisible; - private bool _isPrealignerSetupVisible; - private bool _isInspectionHandoffSetupVisible; - private bool _isInspectionSortRouterSetupVisible; - private bool _isOhtHandoffSetupVisible; - private WaferHandlerDefinition? _savedWaferHandlerSetup; - private PrealignerDefinition? _savedPrealignerSetup; - private InspectionHandoffDefinition? _savedInspectionHandoffSetup; - private InspectionSortRouterDefinition? _savedInspectionSortRouterSetup; - private OhtHandoffDefinition? _savedOhtHandoffSetup; - private string? _waferHandlerHorizontalAxisId; - private string? _waferHandlerVerticalAxisId; - private string? _waferHandlerWorkpieceComponentId; - private string? _waferHandlerSourcePresentSensorChannelId; - private string? _waferHandlerGateOpenSensorChannelId; - private string? _waferHandlerPickCommandChannelId; - private string? _waferHandlerPlaceCommandChannelId; - private string? _waferHandlerHoldingFeedbackChannelId; - private string? _waferHandlerPlacedFeedbackChannelId; - private string _waferHandlerPickHorizontalText = string.Empty; - private string _waferHandlerPickVerticalText = string.Empty; - private string _waferHandlerPlaceHorizontalText = string.Empty; - private string _waferHandlerPlaceVerticalText = string.Empty; - private string? _prealignerRotaryStageComponentId; - private string? _prealignerClampCylinderComponentId; - private string? _prealignerWaferPresentSensorChannelId; - private string? _prealignerAlignmentAcceptedCommandChannelId; - private string? _prealignerAlignmentReadyFeedbackChannelId; - private string? _prealignerAlignmentCompleteFeedbackChannelId; - private string _prealignerAlignmentTargetText = string.Empty; - private string _prealignerAlignmentToleranceText = string.Empty; - private string? _inspectionHandoffCameraId; - private string? _inspectionHandoffPositionSensorChannelId; - private string? _inspectionHandoffAcceptedChannelId; - private string? _inspectionHandoffReadyChannelId; - private string? _inspectionHandoffCompleteChannelId; - private string? _inspectionSortCameraId; - private string? _inspectionSortPassConveyorId; - private string? _inspectionSortNgConveyorId; - private string? _inspectionSortPassFeedbackChannelId; - private string? _inspectionSortNgFeedbackChannelId; - private string? _ohtTransportConveyorId; - private string? _ohtRouteAvailableChannelId; - private string? _ohtVehicleDockedChannelId; - private string? _ohtLoadPortReadyChannelId; - private string? _ohtCarrierReceivedChannelId; - private string? _ohtHandoffReadyChannelId; - private string? _ohtCarrierTransferredChannelId; - - public SemanticEquipmentSetupViewModel( - Func applyWaferHandlerSetup, - Func applyPrealignerSetup, - Func applyInspectionHandoffSetup, - Func applyInspectionSortRouterSetup, - Func applyOhtHandoffSetup, - Action clearWorkbenchPreviews) - { - _applyWaferHandlerSetup = applyWaferHandlerSetup; - _applyPrealignerSetup = applyPrealignerSetup; - _applyInspectionHandoffSetup = applyInspectionHandoffSetup; - _applyInspectionSortRouterSetup = applyInspectionSortRouterSetup; - _applyOhtHandoffSetup = applyOhtHandoffSetup; - _clearWorkbenchPreviews = clearWorkbenchPreviews; - _previewWaferHandlerSetupCommand = new RelayCommand(_ => PreviewWaferHandlerSetup(), _ => IsEditable && _project is not null); - _applyWaferHandlerSetupCommand = new RelayCommand(_ => ApplyWaferHandlerSetup(), ignored => IsEditable && IsWaferHandlerSetupVisible && TryCreateWaferHandlerSetup(out _)); - _cancelWaferHandlerSetupCommand = new RelayCommand(_ => ClearWaferHandlerSetup(), _ => IsWaferHandlerSetupVisible); - _resetWaferHandlerSetupCommand = new RelayCommand(_ => ResetWaferHandlerSetup(), _ => IsEditable && IsWaferHandlerSetupVisible); - _previewPrealignerSetupCommand = new RelayCommand(_ => PreviewPrealignerSetup(), _ => IsEditable && _project is not null); - _applyPrealignerSetupCommand = new RelayCommand(_ => ApplyPrealignerSetup(), ignored => IsEditable && IsPrealignerSetupVisible && TryCreatePrealignerSetup(out _)); - _cancelPrealignerSetupCommand = new RelayCommand(_ => ClearPrealignerSetup(), _ => IsPrealignerSetupVisible); - _resetPrealignerSetupCommand = new RelayCommand(_ => ResetPrealignerSetup(), _ => IsEditable && IsPrealignerSetupVisible); - _previewInspectionHandoffSetupCommand = new RelayCommand(_ => PreviewInspectionHandoffSetup(), _ => IsEditable && _project is not null); - _applyInspectionHandoffSetupCommand = new RelayCommand(_ => ApplyInspectionHandoffSetup(), ignored => IsEditable && IsInspectionHandoffSetupVisible && TryCreateInspectionHandoffSetup(out _)); - _cancelInspectionHandoffSetupCommand = new RelayCommand(_ => ClearInspectionHandoffSetup(), _ => IsInspectionHandoffSetupVisible); - _resetInspectionHandoffSetupCommand = new RelayCommand(_ => ResetInspectionHandoffSetup(), _ => IsEditable && IsInspectionHandoffSetupVisible); - _previewInspectionSortRouterSetupCommand = new RelayCommand(_ => PreviewInspectionSortRouterSetup(), _ => IsEditable && _project is not null); - _applyInspectionSortRouterSetupCommand = new RelayCommand(_ => ApplyInspectionSortRouterSetup(), ignored => IsEditable && IsInspectionSortRouterSetupVisible && TryCreateInspectionSortRouterSetup(out _)); - _cancelInspectionSortRouterSetupCommand = new RelayCommand(_ => ClearInspectionSortRouterSetup(), _ => IsInspectionSortRouterSetupVisible); - _resetInspectionSortRouterSetupCommand = new RelayCommand(_ => ResetInspectionSortRouterSetup(), _ => IsEditable && IsInspectionSortRouterSetupVisible); - _previewOhtHandoffSetupCommand = new RelayCommand(_ => PreviewOhtHandoffSetup(), _ => IsEditable && _project is not null); - _applyOhtHandoffSetupCommand = new RelayCommand(_ => ApplyOhtHandoffSetup(), ignored => IsEditable && IsOhtHandoffSetupVisible && TryCreateOhtHandoffSetup(out _)); - _cancelOhtHandoffSetupCommand = new RelayCommand(_ => ClearOhtHandoffSetup(), _ => IsOhtHandoffSetupVisible); - _resetOhtHandoffSetupCommand = new RelayCommand(_ => ResetOhtHandoffSetup(), _ => IsEditable && IsOhtHandoffSetupVisible); - } - - public ObservableCollection WaferHandlerAxisOptions { get; } = new(); - public ObservableCollection WaferHandlerWorkpieceOptions { get; } = new(); - public ObservableCollection WaferHandlerInputOptions { get; } = new(); - public ObservableCollection WaferHandlerOutputOptions { get; } = new(); - public ObservableCollection PrealignerStageOptions { get; } = new(); - public ObservableCollection PrealignerCylinderOptions { get; } = new(); - public ObservableCollection PrealignerInputOptions { get; } = new(); - public ObservableCollection PrealignerOutputOptions { get; } = new(); - public ObservableCollection InspectionCameraOptions { get; } = new(); - public ObservableCollection InspectionInputOptions { get; } = new(); - public ObservableCollection InspectionOutputOptions { get; } = new(); - public ObservableCollection InspectionConveyorOptions { get; } = new(); - public ICommand PreviewWaferHandlerSetupCommand => _previewWaferHandlerSetupCommand; - public ICommand ApplyWaferHandlerSetupCommand => _applyWaferHandlerSetupCommand; - public ICommand CancelWaferHandlerSetupCommand => _cancelWaferHandlerSetupCommand; - public ICommand ResetWaferHandlerSetupCommand => _resetWaferHandlerSetupCommand; - public ICommand PreviewPrealignerSetupCommand => _previewPrealignerSetupCommand; - public ICommand ApplyPrealignerSetupCommand => _applyPrealignerSetupCommand; - public ICommand CancelPrealignerSetupCommand => _cancelPrealignerSetupCommand; - public ICommand ResetPrealignerSetupCommand => _resetPrealignerSetupCommand; - public ICommand PreviewInspectionHandoffSetupCommand => _previewInspectionHandoffSetupCommand; - public ICommand ApplyInspectionHandoffSetupCommand => _applyInspectionHandoffSetupCommand; - public ICommand CancelInspectionHandoffSetupCommand => _cancelInspectionHandoffSetupCommand; - public ICommand ResetInspectionHandoffSetupCommand => _resetInspectionHandoffSetupCommand; - public ICommand PreviewInspectionSortRouterSetupCommand => _previewInspectionSortRouterSetupCommand; - public ICommand ApplyInspectionSortRouterSetupCommand => _applyInspectionSortRouterSetupCommand; - public ICommand CancelInspectionSortRouterSetupCommand => _cancelInspectionSortRouterSetupCommand; - public ICommand ResetInspectionSortRouterSetupCommand => _resetInspectionSortRouterSetupCommand; - public ICommand PreviewOhtHandoffSetupCommand => _previewOhtHandoffSetupCommand; - public ICommand ApplyOhtHandoffSetupCommand => _applyOhtHandoffSetupCommand; - public ICommand CancelOhtHandoffSetupCommand => _cancelOhtHandoffSetupCommand; - public ICommand ResetOhtHandoffSetupCommand => _resetOhtHandoffSetupCommand; - - public bool IsEditable - { - get => _isEditable; - set - { - if (!SetProperty(ref _isEditable, value)) return; - RaiseCommandStates(); - } - } - - public bool IsWaferHandlerSetupVisible => _isWaferHandlerSetupVisible; - public bool IsPrealignerSetupVisible => _isPrealignerSetupVisible; - public bool IsInspectionHandoffSetupVisible => _isInspectionHandoffSetupVisible; - public bool IsInspectionSortRouterSetupVisible => _isInspectionSortRouterSetupVisible; - public bool IsOhtHandoffSetupVisible => _isOhtHandoffSetupVisible; - public string? WaferHandlerHorizontalAxisId { get => _waferHandlerHorizontalAxisId; set => SetSelection(ref _waferHandlerHorizontalAxisId, value, nameof(WaferHandlerHorizontalAxisId)); } - public string? WaferHandlerVerticalAxisId { get => _waferHandlerVerticalAxisId; set => SetSelection(ref _waferHandlerVerticalAxisId, value, nameof(WaferHandlerVerticalAxisId)); } - public string? WaferHandlerWorkpieceComponentId { get => _waferHandlerWorkpieceComponentId; set => SetSelection(ref _waferHandlerWorkpieceComponentId, value, nameof(WaferHandlerWorkpieceComponentId)); } - public string? WaferHandlerSourcePresentSensorChannelId { get => _waferHandlerSourcePresentSensorChannelId; set => SetSelection(ref _waferHandlerSourcePresentSensorChannelId, value, nameof(WaferHandlerSourcePresentSensorChannelId)); } - public string? WaferHandlerGateOpenSensorChannelId { get => _waferHandlerGateOpenSensorChannelId; set => SetSelection(ref _waferHandlerGateOpenSensorChannelId, value, nameof(WaferHandlerGateOpenSensorChannelId)); } - public string? WaferHandlerPickCommandChannelId { get => _waferHandlerPickCommandChannelId; set => SetSelection(ref _waferHandlerPickCommandChannelId, value, nameof(WaferHandlerPickCommandChannelId)); } - public string? WaferHandlerPlaceCommandChannelId { get => _waferHandlerPlaceCommandChannelId; set => SetSelection(ref _waferHandlerPlaceCommandChannelId, value, nameof(WaferHandlerPlaceCommandChannelId)); } - public string? WaferHandlerHoldingFeedbackChannelId { get => _waferHandlerHoldingFeedbackChannelId; set => SetSelection(ref _waferHandlerHoldingFeedbackChannelId, value, nameof(WaferHandlerHoldingFeedbackChannelId)); } - public string? WaferHandlerPlacedFeedbackChannelId { get => _waferHandlerPlacedFeedbackChannelId; set => SetSelection(ref _waferHandlerPlacedFeedbackChannelId, value, nameof(WaferHandlerPlacedFeedbackChannelId)); } - public string WaferHandlerPickHorizontalText { get => _waferHandlerPickHorizontalText; set => SetText(ref _waferHandlerPickHorizontalText, value, nameof(WaferHandlerPickHorizontalText)); } - public string WaferHandlerPickVerticalText { get => _waferHandlerPickVerticalText; set => SetText(ref _waferHandlerPickVerticalText, value, nameof(WaferHandlerPickVerticalText)); } - public string WaferHandlerPlaceHorizontalText { get => _waferHandlerPlaceHorizontalText; set => SetText(ref _waferHandlerPlaceHorizontalText, value, nameof(WaferHandlerPlaceHorizontalText)); } - public string WaferHandlerPlaceVerticalText { get => _waferHandlerPlaceVerticalText; set => SetText(ref _waferHandlerPlaceVerticalText, value, nameof(WaferHandlerPlaceVerticalText)); } - public bool HasMultipleWaferHandlers => _project?.Devices.Count(device => device.Kind == DeviceKind.Handler) > 1; - public bool IsWaferHandlerHorizontalAxisValid => IsLinearAxis(WaferHandlerHorizontalAxisId); - public bool IsWaferHandlerVerticalAxisValid => IsLinearAxis(WaferHandlerVerticalAxisId) && !Same(WaferHandlerHorizontalAxisId, WaferHandlerVerticalAxisId); - public bool IsWaferHandlerWorkpieceValid => IsLayoutComponent(WaferHandlerWorkpieceComponentId, LayoutComponentKind.Workpiece); - public bool IsWaferHandlerSourcePresentValid => IsChannel(WaferHandlerSourcePresentSensorChannelId, ChannelKind.DigitalInput); - public bool IsWaferHandlerGateOpenValid => IsChannel(WaferHandlerGateOpenSensorChannelId, ChannelKind.DigitalInput); - public bool IsWaferHandlerPickCommandValid => IsChannel(WaferHandlerPickCommandChannelId, ChannelKind.DigitalOutput); - public bool IsWaferHandlerPlaceCommandValid => IsChannel(WaferHandlerPlaceCommandChannelId, ChannelKind.DigitalOutput); - public bool IsWaferHandlerHoldingFeedbackValid => IsChannel(WaferHandlerHoldingFeedbackChannelId, ChannelKind.DigitalInput); - public bool IsWaferHandlerPlacedFeedbackValid => IsChannel(WaferHandlerPlacedFeedbackChannelId, ChannelKind.DigitalInput); - public bool IsWaferHandlerPickHorizontalValid => IsAxisPosition(WaferHandlerHorizontalAxisId, WaferHandlerPickHorizontalText); - public bool IsWaferHandlerPickVerticalValid => IsAxisPosition(WaferHandlerVerticalAxisId, WaferHandlerPickVerticalText); - public bool IsWaferHandlerPlaceHorizontalValid => IsAxisPosition(WaferHandlerHorizontalAxisId, WaferHandlerPlaceHorizontalText); - public bool IsWaferHandlerPlaceVerticalValid => IsAxisPosition(WaferHandlerVerticalAxisId, WaferHandlerPlaceVerticalText); - public bool HasWaferHandlerSetupValidationError => !TryCreateWaferHandlerSetup(out _); - public string WaferHandlerSetupValidationText => ValidationText(HasMultipleWaferHandlers, HasWaferHandlerSetupValidationError, "Connections.WaferHandlerSetupMultipleError", "Connections.WaferHandlerSetupValidationError", "Connections.WaferHandlerSetupValidationReady"); - - public string? PrealignerRotaryStageComponentId { get => _prealignerRotaryStageComponentId; set => SetSelection(ref _prealignerRotaryStageComponentId, value, nameof(PrealignerRotaryStageComponentId)); } - public string? PrealignerClampCylinderComponentId { get => _prealignerClampCylinderComponentId; set => SetSelection(ref _prealignerClampCylinderComponentId, value, nameof(PrealignerClampCylinderComponentId)); } - public string? PrealignerWaferPresentSensorChannelId { get => _prealignerWaferPresentSensorChannelId; set => SetSelection(ref _prealignerWaferPresentSensorChannelId, value, nameof(PrealignerWaferPresentSensorChannelId)); } - public string? PrealignerAlignmentAcceptedCommandChannelId { get => _prealignerAlignmentAcceptedCommandChannelId; set => SetSelection(ref _prealignerAlignmentAcceptedCommandChannelId, value, nameof(PrealignerAlignmentAcceptedCommandChannelId)); } - public string? PrealignerAlignmentReadyFeedbackChannelId { get => _prealignerAlignmentReadyFeedbackChannelId; set => SetSelection(ref _prealignerAlignmentReadyFeedbackChannelId, value, nameof(PrealignerAlignmentReadyFeedbackChannelId)); } - public string? PrealignerAlignmentCompleteFeedbackChannelId { get => _prealignerAlignmentCompleteFeedbackChannelId; set => SetSelection(ref _prealignerAlignmentCompleteFeedbackChannelId, value, nameof(PrealignerAlignmentCompleteFeedbackChannelId)); } - public string PrealignerAlignmentTargetText { get => _prealignerAlignmentTargetText; set => SetText(ref _prealignerAlignmentTargetText, value, nameof(PrealignerAlignmentTargetText)); } - public string PrealignerAlignmentToleranceText { get => _prealignerAlignmentToleranceText; set => SetText(ref _prealignerAlignmentToleranceText, value, nameof(PrealignerAlignmentToleranceText)); } - public bool HasMultiplePrealigners => _project?.Devices.Count(device => device.Kind == DeviceKind.Prealigner) > 1; - public bool IsPrealignerRotaryStageValid => TryGetPrealignerStage(PrealignerRotaryStageComponentId, out _); - public bool IsPrealignerClampCylinderValid => IsLayoutComponent(PrealignerClampCylinderComponentId, LayoutComponentKind.PneumaticCylinder); - public bool IsPrealignerWaferPresentValid => IsChannel(PrealignerWaferPresentSensorChannelId, ChannelKind.DigitalInput); - public bool IsPrealignerAlignmentAcceptedValid => IsChannel(PrealignerAlignmentAcceptedCommandChannelId, ChannelKind.DigitalOutput); - public bool IsPrealignerAlignmentReadyValid => IsChannel(PrealignerAlignmentReadyFeedbackChannelId, ChannelKind.DigitalInput); - public bool IsPrealignerAlignmentCompleteValid => IsChannel(PrealignerAlignmentCompleteFeedbackChannelId, ChannelKind.DigitalInput); - public bool IsPrealignerAlignmentTargetValid => IsRotaryPosition(PrealignerRotaryStageComponentId, PrealignerAlignmentTargetText); - public bool IsPrealignerAlignmentToleranceValid => TryPositiveDouble(PrealignerAlignmentToleranceText, out _); - public bool HasPrealignerSetupValidationError => !TryCreatePrealignerSetup(out _); - public string PrealignerSetupValidationText => ValidationText(HasMultiplePrealigners, HasPrealignerSetupValidationError, "Connections.PrealignerSetupMultipleError", "Connections.PrealignerSetupValidationError", "Connections.PrealignerSetupValidationReady"); - - public string? InspectionHandoffCameraId { get => _inspectionHandoffCameraId; set => SetSelection(ref _inspectionHandoffCameraId, value, nameof(InspectionHandoffCameraId)); } - public string? InspectionHandoffPositionSensorChannelId { get => _inspectionHandoffPositionSensorChannelId; set => SetSelection(ref _inspectionHandoffPositionSensorChannelId, value, nameof(InspectionHandoffPositionSensorChannelId)); } - public string? InspectionHandoffAcceptedChannelId { get => _inspectionHandoffAcceptedChannelId; set => SetSelection(ref _inspectionHandoffAcceptedChannelId, value, nameof(InspectionHandoffAcceptedChannelId)); } - public string? InspectionHandoffReadyChannelId { get => _inspectionHandoffReadyChannelId; set => SetSelection(ref _inspectionHandoffReadyChannelId, value, nameof(InspectionHandoffReadyChannelId)); } - public string? InspectionHandoffCompleteChannelId { get => _inspectionHandoffCompleteChannelId; set => SetSelection(ref _inspectionHandoffCompleteChannelId, value, nameof(InspectionHandoffCompleteChannelId)); } - public bool IsInspectionHandoffCameraValid => IsCamera(InspectionHandoffCameraId); - public bool IsInspectionHandoffPositionValid => IsChannel(InspectionHandoffPositionSensorChannelId, ChannelKind.DigitalInput); - public bool IsInspectionHandoffAcceptedValid => IsChannel(InspectionHandoffAcceptedChannelId, ChannelKind.DigitalOutput); - public bool IsInspectionHandoffReadyValid => IsChannel(InspectionHandoffReadyChannelId, ChannelKind.DigitalInput); - public bool IsInspectionHandoffCompleteValid => IsChannel(InspectionHandoffCompleteChannelId, ChannelKind.DigitalInput); - public bool HasMultipleInspectionHandoffs => _project?.Devices.Count(device => device.Kind == DeviceKind.Inspection) > 1; - public bool HasInspectionHandoffSetupValidationError => !TryCreateInspectionHandoffSetup(out _); - public string InspectionHandoffSetupValidationText => ValidationText(HasMultipleInspectionHandoffs, HasInspectionHandoffSetupValidationError, "Connections.InspectionHandoffSetupMultipleError", "Connections.InspectionHandoffSetupValidationError", "Connections.InspectionHandoffSetupValidationReady"); - - public string? InspectionSortCameraId { get => _inspectionSortCameraId; set => SetSelection(ref _inspectionSortCameraId, value, nameof(InspectionSortCameraId)); } - public string? InspectionSortPassConveyorId { get => _inspectionSortPassConveyorId; set => SetSelection(ref _inspectionSortPassConveyorId, value, nameof(InspectionSortPassConveyorId)); } - public string? InspectionSortNgConveyorId { get => _inspectionSortNgConveyorId; set => SetSelection(ref _inspectionSortNgConveyorId, value, nameof(InspectionSortNgConveyorId)); } - public string? InspectionSortPassFeedbackChannelId { get => _inspectionSortPassFeedbackChannelId; set => SetSelection(ref _inspectionSortPassFeedbackChannelId, value, nameof(InspectionSortPassFeedbackChannelId)); } - public string? InspectionSortNgFeedbackChannelId { get => _inspectionSortNgFeedbackChannelId; set => SetSelection(ref _inspectionSortNgFeedbackChannelId, value, nameof(InspectionSortNgFeedbackChannelId)); } - public bool IsInspectionSortCameraValid => IsCamera(InspectionSortCameraId); - public bool IsInspectionSortPassConveyorValid => IsLayoutComponent(InspectionSortPassConveyorId, LayoutComponentKind.Conveyor); - public bool IsInspectionSortNgConveyorValid => IsLayoutComponent(InspectionSortNgConveyorId, LayoutComponentKind.Conveyor) && !Same(InspectionSortPassConveyorId, InspectionSortNgConveyorId); - public bool IsInspectionSortPassFeedbackValid => IsChannel(InspectionSortPassFeedbackChannelId, ChannelKind.DigitalInput); - public bool IsInspectionSortNgFeedbackValid => IsChannel(InspectionSortNgFeedbackChannelId, ChannelKind.DigitalInput) && !Same(InspectionSortPassFeedbackChannelId, InspectionSortNgFeedbackChannelId); - public bool HasMultipleInspectionSortRouters => _project?.Devices.Count(device => device.Kind == DeviceKind.Sorter) > 1; - public bool HasInspectionSortRouterSetupValidationError => !TryCreateInspectionSortRouterSetup(out _); - public string InspectionSortRouterSetupValidationText => ValidationText(HasMultipleInspectionSortRouters, HasInspectionSortRouterSetupValidationError, "Connections.InspectionSortSetupMultipleError", "Connections.InspectionSortSetupValidationError", "Connections.InspectionSortSetupValidationReady"); - - public string? OhtTransportConveyorId { get => _ohtTransportConveyorId; set => SetSelection(ref _ohtTransportConveyorId, value, nameof(OhtTransportConveyorId)); } - public string? OhtRouteAvailableChannelId { get => _ohtRouteAvailableChannelId; set => SetSelection(ref _ohtRouteAvailableChannelId, value, nameof(OhtRouteAvailableChannelId)); } - public string? OhtVehicleDockedChannelId { get => _ohtVehicleDockedChannelId; set => SetSelection(ref _ohtVehicleDockedChannelId, value, nameof(OhtVehicleDockedChannelId)); } - public string? OhtLoadPortReadyChannelId { get => _ohtLoadPortReadyChannelId; set => SetSelection(ref _ohtLoadPortReadyChannelId, value, nameof(OhtLoadPortReadyChannelId)); } - public string? OhtCarrierReceivedChannelId { get => _ohtCarrierReceivedChannelId; set => SetSelection(ref _ohtCarrierReceivedChannelId, value, nameof(OhtCarrierReceivedChannelId)); } - public string? OhtHandoffReadyChannelId { get => _ohtHandoffReadyChannelId; set => SetSelection(ref _ohtHandoffReadyChannelId, value, nameof(OhtHandoffReadyChannelId)); } - public string? OhtCarrierTransferredChannelId { get => _ohtCarrierTransferredChannelId; set => SetSelection(ref _ohtCarrierTransferredChannelId, value, nameof(OhtCarrierTransferredChannelId)); } - public bool IsOhtTransportConveyorValid => IsLayoutComponent(OhtTransportConveyorId, LayoutComponentKind.Conveyor); - public bool IsOhtRouteAvailableValid => IsChannel(OhtRouteAvailableChannelId, ChannelKind.DigitalInput); - public bool IsOhtVehicleDockedValid => IsChannel(OhtVehicleDockedChannelId, ChannelKind.DigitalInput); - public bool IsOhtLoadPortReadyValid => IsChannel(OhtLoadPortReadyChannelId, ChannelKind.DigitalInput); - public bool IsOhtCarrierReceivedValid => IsChannel(OhtCarrierReceivedChannelId, ChannelKind.DigitalInput); - public bool IsOhtHandoffReadyValid => IsChannel(OhtHandoffReadyChannelId, ChannelKind.DigitalInput); - public bool IsOhtCarrierTransferredValid => IsChannel(OhtCarrierTransferredChannelId, ChannelKind.DigitalInput); - public bool HasMultipleOhtHandoffs => _project?.Devices.Count(device => device.Kind == DeviceKind.Oht) > 1; - public bool HasOhtHandoffSetupValidationError => !TryCreateOhtHandoffSetup(out _); - public string OhtHandoffSetupValidationText => ValidationText(HasMultipleOhtHandoffs, HasOhtHandoffSetupValidationError, "Connections.OhtSetupMultipleError", "Connections.OhtSetupValidationError", "Connections.OhtSetupValidationReady"); - - public void Load(MachineProjectDocument project) - { - _project = project ?? throw new ArgumentNullException(nameof(project)); - ClearAll(); - RaiseCommandStates(); - } - - public void ClearPreviewForCompetingSetup() => ClearAll(); - - internal void RefreshLocalization(Action reloadWorkbench) - { - var waferHandler = IsWaferHandlerSetupVisible ? CaptureWaferHandlerDraft() : null; - var prealigner = IsPrealignerSetupVisible ? CapturePrealignerDraft() : null; - var inspectionHandoff = IsInspectionHandoffSetupVisible ? CaptureInspectionHandoffDraft() : null; - var inspectionSort = IsInspectionSortRouterSetupVisible ? CaptureInspectionSortRouterDraft() : null; - var oht = IsOhtHandoffSetupVisible ? CaptureOhtHandoffDraft() : null; - reloadWorkbench(); - if (waferHandler is not null) { PreviewWaferHandlerSetup(); RestoreWaferHandlerDraft(waferHandler); } - if (prealigner is not null) { PreviewPrealignerSetup(); RestorePrealignerDraft(prealigner); } - if (inspectionHandoff is not null) { PreviewInspectionHandoffSetup(); RestoreInspectionHandoffDraft(inspectionHandoff); } - if (inspectionSort is not null) { PreviewInspectionSortRouterSetup(); RestoreInspectionSortRouterDraft(inspectionSort); } - if (oht is not null) { PreviewOhtHandoffSetup(); RestoreOhtHandoffDraft(oht); } - } - - private void PreviewWaferHandlerSetup() - { - if (_project is null) return; - _clearWorkbenchPreviews(); - ClearPrealignerSetup(); - WaferHandlerAxisOptions.Clear(); WaferHandlerWorkpieceOptions.Clear(); WaferHandlerInputOptions.Clear(); WaferHandlerOutputOptions.Clear(); - foreach (var axis in _project.Axes.Where(axis => axis.Kind == AxisKind.Linear).OrderBy(axis => axis.Name, StringComparer.CurrentCulture).ThenBy(axis => axis.Id, StringComparer.Ordinal)) - WaferHandlerAxisOptions.Add(Option(axis.Id, axis.Name)); - foreach (var component in (ActiveLayout()?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.Workpiece).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) - WaferHandlerWorkpieceOptions.Add(Option(component.Id, component.Name)); - foreach (var channel in _project.Channels.OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) - { - if (channel.Kind == ChannelKind.DigitalInput) WaferHandlerInputOptions.Add(Option(channel.Id, channel.Name)); - if (channel.Kind == ChannelKind.DigitalOutput) WaferHandlerOutputOptions.Add(Option(channel.Id, channel.Name)); - } - var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Handler, WaferHandler: not null }).ToArray(); - _savedWaferHandlerSetup = existing.Length == 1 ? Clone(existing[0].WaferHandler!) : null; - var draft = _savedWaferHandlerSetup ?? SuggestWaferHandlerSetup(); - AddMissing(WaferHandlerAxisOptions, draft.HorizontalAxisId); AddMissing(WaferHandlerAxisOptions, draft.VerticalAxisId); AddMissing(WaferHandlerWorkpieceOptions, draft.WorkpieceComponentId); - foreach (var id in new[] { draft.SourcePresentSensorChannelId, draft.GateOpenSensorChannelId, draft.HoldingFeedbackChannelId, draft.PlacedFeedbackChannelId }) AddMissing(WaferHandlerInputOptions, id); - foreach (var id in new[] { draft.PickCommandChannelId, draft.PlaceCommandChannelId }) AddMissing(WaferHandlerOutputOptions, id); - ApplyDraft(draft); _isWaferHandlerSetupVisible = true; RaiseStateChanged(); - } - - private void PreviewPrealignerSetup() - { - if (_project is null) return; - _clearWorkbenchPreviews(); - ClearWaferHandlerSetup(); - PrealignerStageOptions.Clear(); PrealignerCylinderOptions.Clear(); PrealignerInputOptions.Clear(); PrealignerOutputOptions.Clear(); - foreach (var component in (ActiveLayout()?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.RotaryStage).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) PrealignerStageOptions.Add(Option(component.Id, component.Name)); - foreach (var component in (ActiveLayout()?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.PneumaticCylinder).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) PrealignerCylinderOptions.Add(Option(component.Id, component.Name)); - foreach (var channel in _project.Channels.OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) - { - if (channel.Kind == ChannelKind.DigitalInput) PrealignerInputOptions.Add(Option(channel.Id, channel.Name)); - if (channel.Kind == ChannelKind.DigitalOutput) PrealignerOutputOptions.Add(Option(channel.Id, channel.Name)); - } - var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Prealigner, Prealigner: not null }).ToArray(); - _savedPrealignerSetup = existing.Length == 1 ? Clone(existing[0].Prealigner!) : null; - var draft = _savedPrealignerSetup ?? SuggestPrealignerSetup(); - AddMissing(PrealignerStageOptions, draft.RotaryStageComponentId); AddMissing(PrealignerCylinderOptions, draft.ClampCylinderComponentId); - foreach (var id in new[] { draft.WaferPresentSensorChannelId, draft.AlignmentReadyFeedbackChannelId, draft.AlignmentCompleteFeedbackChannelId }) AddMissing(PrealignerInputOptions, id); - AddMissing(PrealignerOutputOptions, draft.AlignmentAcceptedCommandChannelId); - ApplyDraft(draft); _isPrealignerSetupVisible = true; RaiseStateChanged(); - } - - private void PreviewInspectionHandoffSetup() - { - if (_project is null) return; - PrepareInspectionPreview(); ClearInspectionSortRouterSetup(); ClearOhtHandoffSetup(); - var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Inspection, InspectionHandoff: not null }).ToArray(); - _savedInspectionHandoffSetup = existing.Length == 1 ? Clone(existing[0].InspectionHandoff!) : null; - var draft = _savedInspectionHandoffSetup ?? SuggestInspectionHandoffSetup(); - AddMissing(InspectionCameraOptions, draft.CameraId); foreach (var id in new[] { draft.InspectionPositionSensorChannelId, draft.InspectionReadyFeedbackChannelId, draft.InspectionCompleteFeedbackChannelId }) AddMissing(InspectionInputOptions, id); AddMissing(InspectionOutputOptions, draft.ResultAcceptedCommandChannelId); - ApplyDraft(draft); _isInspectionHandoffSetupVisible = true; RaiseStateChanged(); - } - - private void PreviewInspectionSortRouterSetup() - { - if (_project is null) return; - PrepareInspectionPreview(); ClearInspectionHandoffSetup(); ClearOhtHandoffSetup(); - var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Sorter, InspectionSortRouter: not null }).ToArray(); - _savedInspectionSortRouterSetup = existing.Length == 1 ? Clone(existing[0].InspectionSortRouter!) : null; - var draft = _savedInspectionSortRouterSetup ?? SuggestInspectionSortRouterSetup(); - AddMissing(InspectionCameraOptions, draft.CameraId); AddMissing(InspectionConveyorOptions, draft.PassConveyorComponentId); AddMissing(InspectionConveyorOptions, draft.NgConveyorComponentId); AddMissing(InspectionInputOptions, draft.PassRoutedFeedbackChannelId); AddMissing(InspectionInputOptions, draft.NgRoutedFeedbackChannelId); - ApplyDraft(draft); _isInspectionSortRouterSetupVisible = true; RaiseStateChanged(); - } - - private void PreviewOhtHandoffSetup() - { - if (_project is null) return; - PrepareInspectionPreview(); ClearInspectionHandoffSetup(); ClearInspectionSortRouterSetup(); - var existing = _project.Devices.Where(device => device is { Kind: DeviceKind.Oht, OhtHandoff: not null }).ToArray(); - _savedOhtHandoffSetup = existing.Length == 1 ? Clone(existing[0].OhtHandoff!) : null; - var draft = _savedOhtHandoffSetup ?? SuggestOhtHandoffSetup(); - AddMissing(InspectionConveyorOptions, draft.TransportConveyorComponentId); foreach (var id in new[] { draft.RouteAvailableSensorChannelId, draft.VehicleDockedSensorChannelId, draft.LoadPortReadySensorChannelId, draft.CarrierReceivedSensorChannelId, draft.HandoffReadyFeedbackChannelId, draft.CarrierTransferredFeedbackChannelId }) AddMissing(InspectionInputOptions, id); - ApplyDraft(draft); _isOhtHandoffSetupVisible = true; RaiseStateChanged(); - } - - private void PrepareInspectionPreview() - { - _clearWorkbenchPreviews(); ClearWaferHandlerSetup(); ClearPrealignerSetup(); - InspectionCameraOptions.Clear(); InspectionInputOptions.Clear(); InspectionOutputOptions.Clear(); InspectionConveyorOptions.Clear(); - foreach (var camera in _project!.Devices.Where(device => device is { Kind: DeviceKind.Camera, Camera: not null }).OrderBy(device => device.Name, StringComparer.CurrentCulture).ThenBy(device => device.Id, StringComparer.Ordinal)) InspectionCameraOptions.Add(Option(camera.Id, camera.Name)); - foreach (var component in (ActiveLayout()?.Components ?? []).Where(component => component.Kind == LayoutComponentKind.Conveyor).OrderBy(component => component.Name, StringComparer.CurrentCulture).ThenBy(component => component.Id, StringComparer.Ordinal)) InspectionConveyorOptions.Add(Option(component.Id, component.Name)); - foreach (var channel in _project.Channels.Where(channel => channel.Kind == ChannelKind.DigitalInput).OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) InspectionInputOptions.Add(Option(channel.Id, channel.Name)); - foreach (var channel in _project.Channels.Where(channel => channel.Kind == ChannelKind.DigitalOutput).OrderBy(channel => channel.Name, StringComparer.CurrentCulture).ThenBy(channel => channel.Id, StringComparer.Ordinal)) InspectionOutputOptions.Add(Option(channel.Id, channel.Name)); - } - - private void ApplyWaferHandlerSetup() => Apply(_applyWaferHandlerSetup, TryCreateWaferHandlerSetup, IsEquivalentToSaved, ClearWaferHandlerSetup, PreviewWaferHandlerSetup); - private void ApplyPrealignerSetup() => Apply(_applyPrealignerSetup, TryCreatePrealignerSetup, IsEquivalentToSaved, ClearPrealignerSetup, PreviewPrealignerSetup); - private void ApplyInspectionHandoffSetup() => Apply(_applyInspectionHandoffSetup, TryCreateInspectionHandoffSetup, IsEquivalentToSaved, ClearInspectionHandoffSetup, PreviewInspectionHandoffSetup); - private void ApplyInspectionSortRouterSetup() => Apply(_applyInspectionSortRouterSetup, TryCreateInspectionSortRouterSetup, IsEquivalentToSaved, ClearInspectionSortRouterSetup, PreviewInspectionSortRouterSetup); - private void ApplyOhtHandoffSetup() => Apply(_applyOhtHandoffSetup, TryCreateOhtHandoffSetup, IsEquivalentToSaved, ClearOhtHandoffSetup, PreviewOhtHandoffSetup); - private void ResetWaferHandlerSetup() => ApplyDraft(_savedWaferHandlerSetup is null ? SuggestWaferHandlerSetup() : Clone(_savedWaferHandlerSetup)); - private void ResetPrealignerSetup() => ApplyDraft(_savedPrealignerSetup is null ? SuggestPrealignerSetup() : Clone(_savedPrealignerSetup)); - private void ResetInspectionHandoffSetup() => ApplyDraft(_savedInspectionHandoffSetup is null ? SuggestInspectionHandoffSetup() : Clone(_savedInspectionHandoffSetup)); - private void ResetInspectionSortRouterSetup() => ApplyDraft(_savedInspectionSortRouterSetup is null ? SuggestInspectionSortRouterSetup() : Clone(_savedInspectionSortRouterSetup)); - private void ResetOhtHandoffSetup() => ApplyDraft(_savedOhtHandoffSetup is null ? SuggestOhtHandoffSetup() : Clone(_savedOhtHandoffSetup)); - - private void ClearAll() - { - ClearWaferHandlerSetup(); ClearPrealignerSetup(); ClearInspectionHandoffSetup(); ClearInspectionSortRouterSetup(); ClearOhtHandoffSetup(); - InspectionCameraOptions.Clear(); InspectionInputOptions.Clear(); InspectionOutputOptions.Clear(); InspectionConveyorOptions.Clear(); - } - - private void ClearWaferHandlerSetup() { _isWaferHandlerSetupVisible = false; _savedWaferHandlerSetup = null; WaferHandlerAxisOptions.Clear(); WaferHandlerWorkpieceOptions.Clear(); WaferHandlerInputOptions.Clear(); WaferHandlerOutputOptions.Clear(); RaiseStateChanged(); } - private void ClearPrealignerSetup() { _isPrealignerSetupVisible = false; _savedPrealignerSetup = null; PrealignerStageOptions.Clear(); PrealignerCylinderOptions.Clear(); PrealignerInputOptions.Clear(); PrealignerOutputOptions.Clear(); RaiseStateChanged(); } - private void ClearInspectionHandoffSetup() { _isInspectionHandoffSetupVisible = false; _savedInspectionHandoffSetup = null; RaiseStateChanged(); } - private void ClearInspectionSortRouterSetup() { _isInspectionSortRouterSetupVisible = false; _savedInspectionSortRouterSetup = null; RaiseStateChanged(); } - private void ClearOhtHandoffSetup() { _isOhtHandoffSetupVisible = false; _savedOhtHandoffSetup = null; RaiseStateChanged(); } - - private WaferHandlerDefinition SuggestWaferHandlerSetup() => new() - { - HorizontalAxisId = WaferHandlerAxisOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, VerticalAxisId = WaferHandlerAxisOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, WorkpieceComponentId = WaferHandlerWorkpieceOptions.FirstOrDefault()?.Id ?? string.Empty, - SourcePresentSensorChannelId = WaferHandlerInputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, GateOpenSensorChannelId = WaferHandlerInputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, HoldingFeedbackChannelId = WaferHandlerInputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, PlacedFeedbackChannelId = WaferHandlerInputOptions.ElementAtOrDefault(3)?.Id ?? string.Empty, - PickCommandChannelId = WaferHandlerOutputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, PlaceCommandChannelId = WaferHandlerOutputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, - PickHorizontalPosition = AxisLimit(WaferHandlerAxisOptions.ElementAtOrDefault(0)?.Id, false), PickVerticalPosition = AxisLimit(WaferHandlerAxisOptions.ElementAtOrDefault(1)?.Id, false), PlaceHorizontalPosition = AxisLimit(WaferHandlerAxisOptions.ElementAtOrDefault(0)?.Id, true), PlaceVerticalPosition = AxisLimit(WaferHandlerAxisOptions.ElementAtOrDefault(1)?.Id, true) - }; - private PrealignerDefinition SuggestPrealignerSetup() => new() { RotaryStageComponentId = PrealignerStageOptions.FirstOrDefault()?.Id ?? string.Empty, ClampCylinderComponentId = PrealignerCylinderOptions.FirstOrDefault()?.Id ?? string.Empty, WaferPresentSensorChannelId = PrealignerInputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, AlignmentReadyFeedbackChannelId = PrealignerInputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, AlignmentCompleteFeedbackChannelId = PrealignerInputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, AlignmentAcceptedCommandChannelId = PrealignerOutputOptions.FirstOrDefault()?.Id ?? string.Empty, AlignmentTargetDegrees = StageAxisLimit(PrealignerStageOptions.FirstOrDefault()?.Id), AlignmentToleranceDegrees = 0.1 }; - private InspectionHandoffDefinition SuggestInspectionHandoffSetup() => new() { CameraId = InspectionCameraOptions.FirstOrDefault()?.Id ?? string.Empty, InspectionPositionSensorChannelId = InspectionInputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, InspectionReadyFeedbackChannelId = InspectionInputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, InspectionCompleteFeedbackChannelId = InspectionInputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, ResultAcceptedCommandChannelId = InspectionOutputOptions.FirstOrDefault()?.Id ?? string.Empty }; - private InspectionSortRouterDefinition SuggestInspectionSortRouterSetup() => new() { CameraId = InspectionCameraOptions.FirstOrDefault()?.Id ?? string.Empty, PassConveyorComponentId = InspectionConveyorOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, NgConveyorComponentId = InspectionConveyorOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, PassRoutedFeedbackChannelId = InspectionInputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, NgRoutedFeedbackChannelId = InspectionInputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty }; - private OhtHandoffDefinition SuggestOhtHandoffSetup() => new() { TransportConveyorComponentId = InspectionConveyorOptions.FirstOrDefault()?.Id ?? string.Empty, RouteAvailableSensorChannelId = InspectionInputOptions.ElementAtOrDefault(0)?.Id ?? string.Empty, VehicleDockedSensorChannelId = InspectionInputOptions.ElementAtOrDefault(1)?.Id ?? string.Empty, LoadPortReadySensorChannelId = InspectionInputOptions.ElementAtOrDefault(2)?.Id ?? string.Empty, CarrierReceivedSensorChannelId = InspectionInputOptions.ElementAtOrDefault(3)?.Id ?? string.Empty, HandoffReadyFeedbackChannelId = InspectionInputOptions.ElementAtOrDefault(4)?.Id ?? string.Empty, CarrierTransferredFeedbackChannelId = InspectionInputOptions.ElementAtOrDefault(5)?.Id ?? string.Empty }; - - private void ApplyDraft(WaferHandlerDefinition value) { _waferHandlerHorizontalAxisId = value.HorizontalAxisId; _waferHandlerVerticalAxisId = value.VerticalAxisId; _waferHandlerWorkpieceComponentId = value.WorkpieceComponentId; _waferHandlerSourcePresentSensorChannelId = value.SourcePresentSensorChannelId; _waferHandlerGateOpenSensorChannelId = value.GateOpenSensorChannelId; _waferHandlerPickCommandChannelId = value.PickCommandChannelId; _waferHandlerPlaceCommandChannelId = value.PlaceCommandChannelId; _waferHandlerHoldingFeedbackChannelId = value.HoldingFeedbackChannelId; _waferHandlerPlacedFeedbackChannelId = value.PlacedFeedbackChannelId; _waferHandlerPickHorizontalText = Format(value.PickHorizontalPosition); _waferHandlerPickVerticalText = Format(value.PickVerticalPosition); _waferHandlerPlaceHorizontalText = Format(value.PlaceHorizontalPosition); _waferHandlerPlaceVerticalText = Format(value.PlaceVerticalPosition); RaiseStateChanged(); } - private void ApplyDraft(PrealignerDefinition value) { _prealignerRotaryStageComponentId = value.RotaryStageComponentId; _prealignerClampCylinderComponentId = value.ClampCylinderComponentId; _prealignerWaferPresentSensorChannelId = value.WaferPresentSensorChannelId; _prealignerAlignmentAcceptedCommandChannelId = value.AlignmentAcceptedCommandChannelId; _prealignerAlignmentReadyFeedbackChannelId = value.AlignmentReadyFeedbackChannelId; _prealignerAlignmentCompleteFeedbackChannelId = value.AlignmentCompleteFeedbackChannelId; _prealignerAlignmentTargetText = Format(value.AlignmentTargetDegrees); _prealignerAlignmentToleranceText = Format(value.AlignmentToleranceDegrees); RaiseStateChanged(); } - private void ApplyDraft(InspectionHandoffDefinition value) { _inspectionHandoffCameraId = value.CameraId; _inspectionHandoffPositionSensorChannelId = value.InspectionPositionSensorChannelId; _inspectionHandoffAcceptedChannelId = value.ResultAcceptedCommandChannelId; _inspectionHandoffReadyChannelId = value.InspectionReadyFeedbackChannelId; _inspectionHandoffCompleteChannelId = value.InspectionCompleteFeedbackChannelId; RaiseStateChanged(); } - private void ApplyDraft(InspectionSortRouterDefinition value) { _inspectionSortCameraId = value.CameraId; _inspectionSortPassConveyorId = value.PassConveyorComponentId; _inspectionSortNgConveyorId = value.NgConveyorComponentId; _inspectionSortPassFeedbackChannelId = value.PassRoutedFeedbackChannelId; _inspectionSortNgFeedbackChannelId = value.NgRoutedFeedbackChannelId; RaiseStateChanged(); } - private void ApplyDraft(OhtHandoffDefinition value) { _ohtTransportConveyorId = value.TransportConveyorComponentId; _ohtRouteAvailableChannelId = value.RouteAvailableSensorChannelId; _ohtVehicleDockedChannelId = value.VehicleDockedSensorChannelId; _ohtLoadPortReadyChannelId = value.LoadPortReadySensorChannelId; _ohtCarrierReceivedChannelId = value.CarrierReceivedSensorChannelId; _ohtHandoffReadyChannelId = value.HandoffReadyFeedbackChannelId; _ohtCarrierTransferredChannelId = value.CarrierTransferredFeedbackChannelId; RaiseStateChanged(); } - - private bool TryCreateWaferHandlerSetup(out WaferHandlerDefinition value) - { - var channels = new[] { WaferHandlerSourcePresentSensorChannelId, WaferHandlerGateOpenSensorChannelId, WaferHandlerPickCommandChannelId, WaferHandlerPlaceCommandChannelId, WaferHandlerHoldingFeedbackChannelId, WaferHandlerPlacedFeedbackChannelId }; - value = new WaferHandlerDefinition { HorizontalAxisId = WaferHandlerHorizontalAxisId ?? string.Empty, VerticalAxisId = WaferHandlerVerticalAxisId ?? string.Empty, WorkpieceComponentId = WaferHandlerWorkpieceComponentId ?? string.Empty, SourcePresentSensorChannelId = WaferHandlerSourcePresentSensorChannelId ?? string.Empty, GateOpenSensorChannelId = WaferHandlerGateOpenSensorChannelId ?? string.Empty, PickCommandChannelId = WaferHandlerPickCommandChannelId ?? string.Empty, PlaceCommandChannelId = WaferHandlerPlaceCommandChannelId ?? string.Empty, HoldingFeedbackChannelId = WaferHandlerHoldingFeedbackChannelId ?? string.Empty, PlacedFeedbackChannelId = WaferHandlerPlacedFeedbackChannelId ?? string.Empty, PickHorizontalPosition = Parse(WaferHandlerPickHorizontalText), PickVerticalPosition = Parse(WaferHandlerPickVerticalText), PlaceHorizontalPosition = Parse(WaferHandlerPlaceHorizontalText), PlaceVerticalPosition = Parse(WaferHandlerPlaceVerticalText) }; - return !HasMultipleWaferHandlers && IsWaferHandlerHorizontalAxisValid && IsWaferHandlerVerticalAxisValid && IsWaferHandlerWorkpieceValid && Distinct(channels) && IsWaferHandlerSourcePresentValid && IsWaferHandlerGateOpenValid && IsWaferHandlerPickCommandValid && IsWaferHandlerPlaceCommandValid && IsWaferHandlerHoldingFeedbackValid && IsWaferHandlerPlacedFeedbackValid && IsWaferHandlerPickHorizontalValid && IsWaferHandlerPickVerticalValid && IsWaferHandlerPlaceHorizontalValid && IsWaferHandlerPlaceVerticalValid; - } - private bool TryCreatePrealignerSetup(out PrealignerDefinition value) - { - var channels = new[] { PrealignerWaferPresentSensorChannelId, PrealignerAlignmentAcceptedCommandChannelId, PrealignerAlignmentReadyFeedbackChannelId, PrealignerAlignmentCompleteFeedbackChannelId }; - value = new PrealignerDefinition { RotaryStageComponentId = PrealignerRotaryStageComponentId ?? string.Empty, ClampCylinderComponentId = PrealignerClampCylinderComponentId ?? string.Empty, WaferPresentSensorChannelId = PrealignerWaferPresentSensorChannelId ?? string.Empty, AlignmentAcceptedCommandChannelId = PrealignerAlignmentAcceptedCommandChannelId ?? string.Empty, AlignmentReadyFeedbackChannelId = PrealignerAlignmentReadyFeedbackChannelId ?? string.Empty, AlignmentCompleteFeedbackChannelId = PrealignerAlignmentCompleteFeedbackChannelId ?? string.Empty, AlignmentTargetDegrees = Parse(PrealignerAlignmentTargetText), AlignmentToleranceDegrees = Parse(PrealignerAlignmentToleranceText) }; - return !HasMultiplePrealigners && IsPrealignerRotaryStageValid && IsPrealignerClampCylinderValid && Distinct(channels) && IsPrealignerWaferPresentValid && IsPrealignerAlignmentAcceptedValid && IsPrealignerAlignmentReadyValid && IsPrealignerAlignmentCompleteValid && IsPrealignerAlignmentTargetValid && IsPrealignerAlignmentToleranceValid; - } - private bool TryCreateInspectionHandoffSetup(out InspectionHandoffDefinition value) - { - var channels = new[] { InspectionHandoffPositionSensorChannelId, InspectionHandoffAcceptedChannelId, InspectionHandoffReadyChannelId, InspectionHandoffCompleteChannelId }; - value = new InspectionHandoffDefinition { CameraId = InspectionHandoffCameraId ?? string.Empty, InspectionPositionSensorChannelId = InspectionHandoffPositionSensorChannelId ?? string.Empty, ResultAcceptedCommandChannelId = InspectionHandoffAcceptedChannelId ?? string.Empty, InspectionReadyFeedbackChannelId = InspectionHandoffReadyChannelId ?? string.Empty, InspectionCompleteFeedbackChannelId = InspectionHandoffCompleteChannelId ?? string.Empty }; - return !HasMultipleInspectionHandoffs && IsInspectionHandoffCameraValid && IsInspectionHandoffPositionValid && IsInspectionHandoffAcceptedValid && IsInspectionHandoffReadyValid && IsInspectionHandoffCompleteValid && Distinct(channels); - } - private bool TryCreateInspectionSortRouterSetup(out InspectionSortRouterDefinition value) - { - value = new InspectionSortRouterDefinition { CameraId = InspectionSortCameraId ?? string.Empty, PassConveyorComponentId = InspectionSortPassConveyorId ?? string.Empty, NgConveyorComponentId = InspectionSortNgConveyorId ?? string.Empty, PassRoutedFeedbackChannelId = InspectionSortPassFeedbackChannelId ?? string.Empty, NgRoutedFeedbackChannelId = InspectionSortNgFeedbackChannelId ?? string.Empty }; - return !HasMultipleInspectionSortRouters && IsInspectionSortCameraValid && IsInspectionSortPassConveyorValid && IsInspectionSortNgConveyorValid && IsInspectionSortPassFeedbackValid && IsInspectionSortNgFeedbackValid; - } - private bool TryCreateOhtHandoffSetup(out OhtHandoffDefinition value) - { - var channels = new[] { OhtRouteAvailableChannelId, OhtVehicleDockedChannelId, OhtLoadPortReadyChannelId, OhtCarrierReceivedChannelId, OhtHandoffReadyChannelId, OhtCarrierTransferredChannelId }; - value = new OhtHandoffDefinition { TransportConveyorComponentId = OhtTransportConveyorId ?? string.Empty, RouteAvailableSensorChannelId = OhtRouteAvailableChannelId ?? string.Empty, VehicleDockedSensorChannelId = OhtVehicleDockedChannelId ?? string.Empty, LoadPortReadySensorChannelId = OhtLoadPortReadyChannelId ?? string.Empty, CarrierReceivedSensorChannelId = OhtCarrierReceivedChannelId ?? string.Empty, HandoffReadyFeedbackChannelId = OhtHandoffReadyChannelId ?? string.Empty, CarrierTransferredFeedbackChannelId = OhtCarrierTransferredChannelId ?? string.Empty }; - return !HasMultipleOhtHandoffs && IsOhtTransportConveyorValid && Distinct(channels) && IsOhtRouteAvailableValid && IsOhtVehicleDockedValid && IsOhtLoadPortReadyValid && IsOhtCarrierReceivedValid && IsOhtHandoffReadyValid && IsOhtCarrierTransferredValid; - } - - private bool IsEquivalentToSaved(WaferHandlerDefinition value) => _savedWaferHandlerSetup is not null && Same(_savedWaferHandlerSetup.HorizontalAxisId, value.HorizontalAxisId) && Same(_savedWaferHandlerSetup.VerticalAxisId, value.VerticalAxisId) && Same(_savedWaferHandlerSetup.WorkpieceComponentId, value.WorkpieceComponentId) && Same(_savedWaferHandlerSetup.SourcePresentSensorChannelId, value.SourcePresentSensorChannelId) && Same(_savedWaferHandlerSetup.GateOpenSensorChannelId, value.GateOpenSensorChannelId) && Same(_savedWaferHandlerSetup.PickCommandChannelId, value.PickCommandChannelId) && Same(_savedWaferHandlerSetup.PlaceCommandChannelId, value.PlaceCommandChannelId) && Same(_savedWaferHandlerSetup.HoldingFeedbackChannelId, value.HoldingFeedbackChannelId) && Same(_savedWaferHandlerSetup.PlacedFeedbackChannelId, value.PlacedFeedbackChannelId) && _savedWaferHandlerSetup.PickHorizontalPosition == value.PickHorizontalPosition && _savedWaferHandlerSetup.PickVerticalPosition == value.PickVerticalPosition && _savedWaferHandlerSetup.PlaceHorizontalPosition == value.PlaceHorizontalPosition && _savedWaferHandlerSetup.PlaceVerticalPosition == value.PlaceVerticalPosition; - private bool IsEquivalentToSaved(PrealignerDefinition value) => _savedPrealignerSetup is not null && Same(_savedPrealignerSetup.RotaryStageComponentId, value.RotaryStageComponentId) && Same(_savedPrealignerSetup.ClampCylinderComponentId, value.ClampCylinderComponentId) && Same(_savedPrealignerSetup.WaferPresentSensorChannelId, value.WaferPresentSensorChannelId) && Same(_savedPrealignerSetup.AlignmentAcceptedCommandChannelId, value.AlignmentAcceptedCommandChannelId) && Same(_savedPrealignerSetup.AlignmentReadyFeedbackChannelId, value.AlignmentReadyFeedbackChannelId) && Same(_savedPrealignerSetup.AlignmentCompleteFeedbackChannelId, value.AlignmentCompleteFeedbackChannelId) && _savedPrealignerSetup.AlignmentTargetDegrees == value.AlignmentTargetDegrees && _savedPrealignerSetup.AlignmentToleranceDegrees == value.AlignmentToleranceDegrees; - private bool IsEquivalentToSaved(InspectionHandoffDefinition value) => _savedInspectionHandoffSetup is not null && Same(_savedInspectionHandoffSetup.CameraId, value.CameraId) && Same(_savedInspectionHandoffSetup.InspectionPositionSensorChannelId, value.InspectionPositionSensorChannelId) && Same(_savedInspectionHandoffSetup.ResultAcceptedCommandChannelId, value.ResultAcceptedCommandChannelId) && Same(_savedInspectionHandoffSetup.InspectionReadyFeedbackChannelId, value.InspectionReadyFeedbackChannelId) && Same(_savedInspectionHandoffSetup.InspectionCompleteFeedbackChannelId, value.InspectionCompleteFeedbackChannelId); - private bool IsEquivalentToSaved(InspectionSortRouterDefinition value) => _savedInspectionSortRouterSetup is not null && Same(_savedInspectionSortRouterSetup.CameraId, value.CameraId) && Same(_savedInspectionSortRouterSetup.PassConveyorComponentId, value.PassConveyorComponentId) && Same(_savedInspectionSortRouterSetup.NgConveyorComponentId, value.NgConveyorComponentId) && Same(_savedInspectionSortRouterSetup.PassRoutedFeedbackChannelId, value.PassRoutedFeedbackChannelId) && Same(_savedInspectionSortRouterSetup.NgRoutedFeedbackChannelId, value.NgRoutedFeedbackChannelId); - private bool IsEquivalentToSaved(OhtHandoffDefinition value) => _savedOhtHandoffSetup is not null && Same(_savedOhtHandoffSetup.TransportConveyorComponentId, value.TransportConveyorComponentId) && Same(_savedOhtHandoffSetup.RouteAvailableSensorChannelId, value.RouteAvailableSensorChannelId) && Same(_savedOhtHandoffSetup.VehicleDockedSensorChannelId, value.VehicleDockedSensorChannelId) && Same(_savedOhtHandoffSetup.LoadPortReadySensorChannelId, value.LoadPortReadySensorChannelId) && Same(_savedOhtHandoffSetup.CarrierReceivedSensorChannelId, value.CarrierReceivedSensorChannelId) && Same(_savedOhtHandoffSetup.HandoffReadyFeedbackChannelId, value.HandoffReadyFeedbackChannelId) && Same(_savedOhtHandoffSetup.CarrierTransferredFeedbackChannelId, value.CarrierTransferredFeedbackChannelId); - - private WaferHandlerDraft CaptureWaferHandlerDraft() => new(WaferHandlerHorizontalAxisId, WaferHandlerVerticalAxisId, WaferHandlerWorkpieceComponentId, WaferHandlerSourcePresentSensorChannelId, WaferHandlerGateOpenSensorChannelId, WaferHandlerPickCommandChannelId, WaferHandlerPlaceCommandChannelId, WaferHandlerHoldingFeedbackChannelId, WaferHandlerPlacedFeedbackChannelId, WaferHandlerPickHorizontalText, WaferHandlerPickVerticalText, WaferHandlerPlaceHorizontalText, WaferHandlerPlaceVerticalText); - private PrealignerDraft CapturePrealignerDraft() => new(PrealignerRotaryStageComponentId, PrealignerClampCylinderComponentId, PrealignerWaferPresentSensorChannelId, PrealignerAlignmentAcceptedCommandChannelId, PrealignerAlignmentReadyFeedbackChannelId, PrealignerAlignmentCompleteFeedbackChannelId, PrealignerAlignmentTargetText, PrealignerAlignmentToleranceText); - private InspectionHandoffDraft CaptureInspectionHandoffDraft() => new(InspectionHandoffCameraId, InspectionHandoffPositionSensorChannelId, InspectionHandoffAcceptedChannelId, InspectionHandoffReadyChannelId, InspectionHandoffCompleteChannelId); - private InspectionSortRouterDraft CaptureInspectionSortRouterDraft() => new(InspectionSortCameraId, InspectionSortPassConveyorId, InspectionSortNgConveyorId, InspectionSortPassFeedbackChannelId, InspectionSortNgFeedbackChannelId); - private OhtHandoffDraft CaptureOhtHandoffDraft() => new(OhtTransportConveyorId, OhtRouteAvailableChannelId, OhtVehicleDockedChannelId, OhtLoadPortReadyChannelId, OhtCarrierReceivedChannelId, OhtHandoffReadyChannelId, OhtCarrierTransferredChannelId); - private void RestoreWaferHandlerDraft(WaferHandlerDraft value) { _waferHandlerHorizontalAxisId = value.HorizontalAxisId; _waferHandlerVerticalAxisId = value.VerticalAxisId; _waferHandlerWorkpieceComponentId = value.WorkpieceId; _waferHandlerSourcePresentSensorChannelId = value.SourceInputId; _waferHandlerGateOpenSensorChannelId = value.GateInputId; _waferHandlerPickCommandChannelId = value.PickOutputId; _waferHandlerPlaceCommandChannelId = value.PlaceOutputId; _waferHandlerHoldingFeedbackChannelId = value.HoldingInputId; _waferHandlerPlacedFeedbackChannelId = value.PlacedInputId; _waferHandlerPickHorizontalText = value.PickHorizontal; _waferHandlerPickVerticalText = value.PickVertical; _waferHandlerPlaceHorizontalText = value.PlaceHorizontal; _waferHandlerPlaceVerticalText = value.PlaceVertical; RaiseStateChanged(); } - private void RestorePrealignerDraft(PrealignerDraft value) { _prealignerRotaryStageComponentId = value.StageId; _prealignerClampCylinderComponentId = value.ClampId; _prealignerWaferPresentSensorChannelId = value.WaferPresentId; _prealignerAlignmentAcceptedCommandChannelId = value.AcceptedId; _prealignerAlignmentReadyFeedbackChannelId = value.ReadyId; _prealignerAlignmentCompleteFeedbackChannelId = value.CompleteId; _prealignerAlignmentTargetText = value.Target; _prealignerAlignmentToleranceText = value.Tolerance; RaiseStateChanged(); } - private void RestoreInspectionHandoffDraft(InspectionHandoffDraft value) { AddMissing(InspectionCameraOptions, value.CameraId); foreach (var id in new[] { value.PositionInputId, value.ReadyInputId, value.CompleteInputId }) AddMissing(InspectionInputOptions, id); AddMissing(InspectionOutputOptions, value.AcceptedOutputId); _inspectionHandoffCameraId = value.CameraId; _inspectionHandoffPositionSensorChannelId = value.PositionInputId; _inspectionHandoffAcceptedChannelId = value.AcceptedOutputId; _inspectionHandoffReadyChannelId = value.ReadyInputId; _inspectionHandoffCompleteChannelId = value.CompleteInputId; RaiseStateChanged(); } - private void RestoreInspectionSortRouterDraft(InspectionSortRouterDraft value) { AddMissing(InspectionCameraOptions, value.CameraId); AddMissing(InspectionConveyorOptions, value.PassConveyorId); AddMissing(InspectionConveyorOptions, value.NgConveyorId); AddMissing(InspectionInputOptions, value.PassFeedbackInputId); AddMissing(InspectionInputOptions, value.NgFeedbackInputId); _inspectionSortCameraId = value.CameraId; _inspectionSortPassConveyorId = value.PassConveyorId; _inspectionSortNgConveyorId = value.NgConveyorId; _inspectionSortPassFeedbackChannelId = value.PassFeedbackInputId; _inspectionSortNgFeedbackChannelId = value.NgFeedbackInputId; RaiseStateChanged(); } - private void RestoreOhtHandoffDraft(OhtHandoffDraft value) { AddMissing(InspectionConveyorOptions, value.TransportConveyorId); foreach (var id in new[] { value.RouteAvailableInputId, value.VehicleDockedInputId, value.LoadPortReadyInputId, value.CarrierReceivedInputId, value.HandoffReadyInputId, value.CarrierTransferredInputId }) AddMissing(InspectionInputOptions, id); _ohtTransportConveyorId = value.TransportConveyorId; _ohtRouteAvailableChannelId = value.RouteAvailableInputId; _ohtVehicleDockedChannelId = value.VehicleDockedInputId; _ohtLoadPortReadyChannelId = value.LoadPortReadyInputId; _ohtCarrierReceivedChannelId = value.CarrierReceivedInputId; _ohtHandoffReadyChannelId = value.HandoffReadyInputId; _ohtCarrierTransferredChannelId = value.CarrierTransferredInputId; RaiseStateChanged(); } - - private void SetSelection(ref string? field, string? value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } - private void SetText(ref string field, string value, string propertyName) { if (SetProperty(ref field, value, propertyName)) RaiseValidationChanged(); } - private MachineLayoutDefinition? ActiveLayout() => _project is null ? null : ResolveActiveLayout(_project); - private bool IsLinearAxis(string? id) => _project?.Axes.Any(axis => axis.Kind == AxisKind.Linear && Same(axis.Id, id)) == true; - private bool IsLayoutComponent(string? id, LayoutComponentKind kind) => ActiveLayout()?.Components.Any(component => component.Kind == kind && Same(component.Id, id)) == true; - private bool IsChannel(string? id, ChannelKind kind) => _project?.Channels.Any(channel => channel.Kind == kind && Same(channel.Id, id)) == true; - private bool IsCamera(string? id) => _project?.Devices.Any(device => device is { Kind: DeviceKind.Camera, Camera: not null } && Same(device.Id, id)) == true; - private bool IsAxisPosition(string? axisId, string text) => TryGetAxis(axisId, out var axis) && TryFiniteDouble(text, out var value) && axis.SoftLimitMin.HasValue && axis.SoftLimitMax.HasValue && value >= axis.SoftLimitMin.Value && value <= axis.SoftLimitMax.Value; - private bool IsRotaryPosition(string? stageId, string text) => TryGetPrealignerStage(stageId, out var stage) && TryFiniteDouble(text, out var value) && _project!.Axes.FirstOrDefault(axis => Same(axis.Id, stage.BehaviorBindingId)) is { Kind: AxisKind.Rotary, SoftLimitMin: not null, SoftLimitMax: not null } axis && value >= axis.SoftLimitMin.Value && value <= axis.SoftLimitMax.Value; - private bool TryGetAxis(string? id, out OpenVisionLab.Machine.Core.Axes.VirtualAxisDefinition axis) { axis = _project?.Axes.FirstOrDefault(candidate => Same(candidate.Id, id))!; return axis is not null; } - private bool TryGetPrealignerStage(string? id, out LayoutComponentDefinition stage) { var value = ActiveLayout()?.Components.FirstOrDefault(component => component.Kind == LayoutComponentKind.RotaryStage && Same(component.Id, id)); stage = value!; return value is not null && _project!.Axes.Any(axis => axis.Kind == AxisKind.Rotary && Same(axis.Id, value.BehaviorBindingId)); } - private double AxisLimit(string? axisId, bool maximum) => TryGetAxis(axisId, out var axis) && axis.SoftLimitMin.HasValue && axis.SoftLimitMax.HasValue ? maximum ? axis.SoftLimitMax.Value : axis.SoftLimitMin.Value : 0; - private double StageAxisLimit(string? stageId) => TryGetPrealignerStage(stageId, out var stage) && _project!.Axes.FirstOrDefault(axis => Same(axis.Id, stage.BehaviorBindingId)) is { SoftLimitMin: not null } axis ? axis.SoftLimitMin.Value : 0; - - private void RaiseStateChanged() - { - OnPropertyChanged(nameof(IsWaferHandlerSetupVisible)); OnPropertyChanged(nameof(IsPrealignerSetupVisible)); OnPropertyChanged(nameof(IsInspectionHandoffSetupVisible)); OnPropertyChanged(nameof(IsInspectionSortRouterSetupVisible)); OnPropertyChanged(nameof(IsOhtHandoffSetupVisible)); - RaiseValidationChanged(); RaiseCommandStates(); - } - - private void RaiseValidationChanged() - { - foreach (var property in ValidationProperties) OnPropertyChanged(property); - _applyWaferHandlerSetupCommand.RaiseCanExecuteChanged(); _applyPrealignerSetupCommand.RaiseCanExecuteChanged(); _applyInspectionHandoffSetupCommand.RaiseCanExecuteChanged(); _applyInspectionSortRouterSetupCommand.RaiseCanExecuteChanged(); _applyOhtHandoffSetupCommand.RaiseCanExecuteChanged(); - } - - private void RaiseCommandStates() - { - foreach (var command in new[] { _previewWaferHandlerSetupCommand, _applyWaferHandlerSetupCommand, _cancelWaferHandlerSetupCommand, _resetWaferHandlerSetupCommand, _previewPrealignerSetupCommand, _applyPrealignerSetupCommand, _cancelPrealignerSetupCommand, _resetPrealignerSetupCommand, _previewInspectionHandoffSetupCommand, _applyInspectionHandoffSetupCommand, _cancelInspectionHandoffSetupCommand, _resetInspectionHandoffSetupCommand, _previewInspectionSortRouterSetupCommand, _applyInspectionSortRouterSetupCommand, _cancelInspectionSortRouterSetupCommand, _resetInspectionSortRouterSetupCommand, _previewOhtHandoffSetupCommand, _applyOhtHandoffSetupCommand, _cancelOhtHandoffSetupCommand, _resetOhtHandoffSetupCommand }) command.RaiseCanExecuteChanged(); - } - - private delegate bool TryCreate(out T value) where T : class; - private static void Apply(Func apply, TryCreate create, Func equivalent, Action clear, Action preview) where T : class - { - if (!create(out var value)) return; - if (apply(value) > 0 || equivalent(value)) clear(); else preview(); - } - private static bool Distinct(string?[] values) => values.All(value => !string.IsNullOrWhiteSpace(value)) && values.Distinct(StringComparer.Ordinal).Count() == values.Length; - private static bool Same(string? left, string? right) => string.Equals(left, right, StringComparison.Ordinal); - private static bool TryPositiveDouble(string text, out double value) => TryFiniteDouble(text, out value) && value > 0; - private static bool TryFiniteDouble(string text, out double value) => (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) || double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) && double.IsFinite(value); - private static double Parse(string text) => TryFiniteDouble(text, out var value) ? value : double.NaN; - private static string Format(double value) => value.ToString("0.###", CultureInfo.CurrentCulture); - private static LoadLockSetupOption Option(string id, string? name) => new(id, string.IsNullOrWhiteSpace(name) ? id : $"{name} — {id}"); - private static void AddMissing(ObservableCollection options, string? id) { if (!string.IsNullOrWhiteSpace(id) && options.All(option => !Same(option.Id, id))) options.Add(new LoadLockSetupOption(id, $"{id} ({OpenVisionLanguageService.T("Connections.LoadLockSetupMissing")})")); } - private static string ValidationText(bool multiple, bool invalid, string multipleKey, string invalidKey, string readyKey) => OpenVisionLanguageService.T(multiple ? multipleKey : invalid ? invalidKey : readyKey); - private static MachineLayoutDefinition? ResolveActiveLayout(MachineProjectDocument project) => !string.IsNullOrWhiteSpace(project.Simulation.ActiveLayoutId) ? project.Layouts.FirstOrDefault(layout => Same(layout.Id, project.Simulation.ActiveLayoutId)) : project.Layouts.Count == 1 ? project.Layouts[0] : null; - - private static WaferHandlerDefinition Clone(WaferHandlerDefinition value) => new() { HorizontalAxisId = value.HorizontalAxisId, VerticalAxisId = value.VerticalAxisId, WorkpieceComponentId = value.WorkpieceComponentId, SourcePresentSensorChannelId = value.SourcePresentSensorChannelId, GateOpenSensorChannelId = value.GateOpenSensorChannelId, PickCommandChannelId = value.PickCommandChannelId, PlaceCommandChannelId = value.PlaceCommandChannelId, HoldingFeedbackChannelId = value.HoldingFeedbackChannelId, PlacedFeedbackChannelId = value.PlacedFeedbackChannelId, PickHorizontalPosition = value.PickHorizontalPosition, PickVerticalPosition = value.PickVerticalPosition, PlaceHorizontalPosition = value.PlaceHorizontalPosition, PlaceVerticalPosition = value.PlaceVerticalPosition }; - private static PrealignerDefinition Clone(PrealignerDefinition value) => new() { RotaryStageComponentId = value.RotaryStageComponentId, ClampCylinderComponentId = value.ClampCylinderComponentId, WaferPresentSensorChannelId = value.WaferPresentSensorChannelId, AlignmentAcceptedCommandChannelId = value.AlignmentAcceptedCommandChannelId, AlignmentReadyFeedbackChannelId = value.AlignmentReadyFeedbackChannelId, AlignmentCompleteFeedbackChannelId = value.AlignmentCompleteFeedbackChannelId, AlignmentTargetDegrees = value.AlignmentTargetDegrees, AlignmentToleranceDegrees = value.AlignmentToleranceDegrees }; - private static InspectionHandoffDefinition Clone(InspectionHandoffDefinition value) => new() { CameraId = value.CameraId, InspectionPositionSensorChannelId = value.InspectionPositionSensorChannelId, ResultAcceptedCommandChannelId = value.ResultAcceptedCommandChannelId, InspectionReadyFeedbackChannelId = value.InspectionReadyFeedbackChannelId, InspectionCompleteFeedbackChannelId = value.InspectionCompleteFeedbackChannelId }; - private static InspectionSortRouterDefinition Clone(InspectionSortRouterDefinition value) => new() { CameraId = value.CameraId, PassConveyorComponentId = value.PassConveyorComponentId, NgConveyorComponentId = value.NgConveyorComponentId, PassRoutedFeedbackChannelId = value.PassRoutedFeedbackChannelId, NgRoutedFeedbackChannelId = value.NgRoutedFeedbackChannelId }; - private static OhtHandoffDefinition Clone(OhtHandoffDefinition value) => new() { TransportConveyorComponentId = value.TransportConveyorComponentId, RouteAvailableSensorChannelId = value.RouteAvailableSensorChannelId, VehicleDockedSensorChannelId = value.VehicleDockedSensorChannelId, LoadPortReadySensorChannelId = value.LoadPortReadySensorChannelId, CarrierReceivedSensorChannelId = value.CarrierReceivedSensorChannelId, HandoffReadyFeedbackChannelId = value.HandoffReadyFeedbackChannelId, CarrierTransferredFeedbackChannelId = value.CarrierTransferredFeedbackChannelId }; - - private sealed record WaferHandlerDraft(string? HorizontalAxisId, string? VerticalAxisId, string? WorkpieceId, string? SourceInputId, string? GateInputId, string? PickOutputId, string? PlaceOutputId, string? HoldingInputId, string? PlacedInputId, string PickHorizontal, string PickVertical, string PlaceHorizontal, string PlaceVertical); - private sealed record PrealignerDraft(string? StageId, string? ClampId, string? WaferPresentId, string? AcceptedId, string? ReadyId, string? CompleteId, string Target, string Tolerance); - private sealed record InspectionHandoffDraft(string? CameraId, string? PositionInputId, string? AcceptedOutputId, string? ReadyInputId, string? CompleteInputId); - private sealed record InspectionSortRouterDraft(string? CameraId, string? PassConveyorId, string? NgConveyorId, string? PassFeedbackInputId, string? NgFeedbackInputId); - private sealed record OhtHandoffDraft(string? TransportConveyorId, string? RouteAvailableInputId, string? VehicleDockedInputId, string? LoadPortReadyInputId, string? CarrierReceivedInputId, string? HandoffReadyInputId, string? CarrierTransferredInputId); - - private static readonly string[] ValidationProperties = - [ - nameof(WaferHandlerHorizontalAxisId), nameof(WaferHandlerVerticalAxisId), nameof(WaferHandlerWorkpieceComponentId), nameof(WaferHandlerSourcePresentSensorChannelId), nameof(WaferHandlerGateOpenSensorChannelId), nameof(WaferHandlerPickCommandChannelId), nameof(WaferHandlerPlaceCommandChannelId), nameof(WaferHandlerHoldingFeedbackChannelId), nameof(WaferHandlerPlacedFeedbackChannelId), nameof(WaferHandlerPickHorizontalText), nameof(WaferHandlerPickVerticalText), nameof(WaferHandlerPlaceHorizontalText), nameof(WaferHandlerPlaceVerticalText), nameof(IsWaferHandlerHorizontalAxisValid), nameof(IsWaferHandlerVerticalAxisValid), nameof(IsWaferHandlerWorkpieceValid), nameof(IsWaferHandlerSourcePresentValid), nameof(IsWaferHandlerGateOpenValid), nameof(IsWaferHandlerPickCommandValid), nameof(IsWaferHandlerPlaceCommandValid), nameof(IsWaferHandlerHoldingFeedbackValid), nameof(IsWaferHandlerPlacedFeedbackValid), nameof(IsWaferHandlerPickHorizontalValid), nameof(IsWaferHandlerPickVerticalValid), nameof(IsWaferHandlerPlaceHorizontalValid), nameof(IsWaferHandlerPlaceVerticalValid), nameof(HasMultipleWaferHandlers), nameof(HasWaferHandlerSetupValidationError), nameof(WaferHandlerSetupValidationText), - nameof(PrealignerRotaryStageComponentId), nameof(PrealignerClampCylinderComponentId), nameof(PrealignerWaferPresentSensorChannelId), nameof(PrealignerAlignmentAcceptedCommandChannelId), nameof(PrealignerAlignmentReadyFeedbackChannelId), nameof(PrealignerAlignmentCompleteFeedbackChannelId), nameof(PrealignerAlignmentTargetText), nameof(PrealignerAlignmentToleranceText), nameof(IsPrealignerRotaryStageValid), nameof(IsPrealignerClampCylinderValid), nameof(IsPrealignerWaferPresentValid), nameof(IsPrealignerAlignmentAcceptedValid), nameof(IsPrealignerAlignmentReadyValid), nameof(IsPrealignerAlignmentCompleteValid), nameof(IsPrealignerAlignmentTargetValid), nameof(IsPrealignerAlignmentToleranceValid), nameof(HasMultiplePrealigners), nameof(HasPrealignerSetupValidationError), nameof(PrealignerSetupValidationText), - nameof(InspectionHandoffCameraId), nameof(InspectionHandoffPositionSensorChannelId), nameof(InspectionHandoffAcceptedChannelId), nameof(InspectionHandoffReadyChannelId), nameof(InspectionHandoffCompleteChannelId), nameof(IsInspectionHandoffCameraValid), nameof(IsInspectionHandoffPositionValid), nameof(IsInspectionHandoffAcceptedValid), nameof(IsInspectionHandoffReadyValid), nameof(IsInspectionHandoffCompleteValid), nameof(HasMultipleInspectionHandoffs), nameof(HasInspectionHandoffSetupValidationError), nameof(InspectionHandoffSetupValidationText), - nameof(InspectionSortCameraId), nameof(InspectionSortPassConveyorId), nameof(InspectionSortNgConveyorId), nameof(InspectionSortPassFeedbackChannelId), nameof(InspectionSortNgFeedbackChannelId), nameof(IsInspectionSortCameraValid), nameof(IsInspectionSortPassConveyorValid), nameof(IsInspectionSortNgConveyorValid), nameof(IsInspectionSortPassFeedbackValid), nameof(IsInspectionSortNgFeedbackValid), nameof(HasMultipleInspectionSortRouters), nameof(HasInspectionSortRouterSetupValidationError), nameof(InspectionSortRouterSetupValidationText), - nameof(OhtTransportConveyorId), nameof(OhtRouteAvailableChannelId), nameof(OhtVehicleDockedChannelId), nameof(OhtLoadPortReadyChannelId), nameof(OhtCarrierReceivedChannelId), nameof(OhtHandoffReadyChannelId), nameof(OhtCarrierTransferredChannelId), nameof(IsOhtTransportConveyorValid), nameof(IsOhtRouteAvailableValid), nameof(IsOhtVehicleDockedValid), nameof(IsOhtLoadPortReadyValid), nameof(IsOhtCarrierReceivedValid), nameof(IsOhtHandoffReadyValid), nameof(IsOhtCarrierTransferredValid), nameof(HasMultipleOhtHandoffs), nameof(HasOhtHandoffSetupValidationError), nameof(OhtHandoffSetupValidationText) - ]; -} diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/LoadLockSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Station/LoadLockSetupViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/LoadLockSetupViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Station/LoadLockSetupViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/StationSkeletonSetupViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Station/StationSkeletonSetupViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Recipes/StationSkeletonSetupViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Recipes/Station/StationSkeletonSetupViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/FaultManagerViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Faults/FaultManagerViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/FaultManagerViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Faults/FaultManagerViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/ScheduledFaultScenarioWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Faults/ScheduledFaultScenarioWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/ScheduledFaultScenarioWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Faults/ScheduledFaultScenarioWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/BoundedShutdownCoordinator.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/BoundedShutdownCoordinator.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/BoundedShutdownCoordinator.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/BoundedShutdownCoordinator.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeAlarmCollectionViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeAlarmCollectionViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeAlarmCollectionViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeAlarmCollectionViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeDebuggerViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeDebuggerViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeDebuggerViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeDebuggerViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeDebuggerWatchTargetCatalog.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeDebuggerWatchTargetCatalog.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeDebuggerWatchTargetCatalog.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeDebuggerWatchTargetCatalog.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeDefinitionApplicationWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeDefinitionApplicationWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeDefinitionApplicationWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeDefinitionApplicationWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeObservabilityJournal.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeObservabilityJournal.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeObservabilityJournal.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeObservabilityJournal.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeObservabilityPresenter.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeObservabilityPresenter.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeObservabilityPresenter.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeObservabilityPresenter.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeTimelineViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeTimelineViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/RuntimeTimelineViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/RuntimeTimelineViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRunControlAdmissionPolicy.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRunControlAdmissionPolicy.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRunControlAdmissionPolicy.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRunControlAdmissionPolicy.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRunControlWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRunControlWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRunControlWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRunControlWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeLoop.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeLoop.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeLoop.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeLoop.cs index cfb6ff1..c468247 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeLoop.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeLoop.cs @@ -177,7 +177,7 @@ await _dispatch(() => private async Task ObserveEngineTerminationAsync() { var termination = await _engine.Termination.ConfigureAwait(false); - _onTerminated(termination); + await _dispatch(() => _onTerminated(termination)).ConfigureAwait(false); } private async Task ConsumeSnapshotsAsync() diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeProjectionCoordinator.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeProjectionCoordinator.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeProjectionCoordinator.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeProjectionCoordinator.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeResourceOwner.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeResourceOwner.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeResourceOwner.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeResourceOwner.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeShutdownWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeShutdownWorkflow.cs similarity index 90% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeShutdownWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeShutdownWorkflow.cs index ddf55b7..911fa97 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeShutdownWorkflow.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeShutdownWorkflow.cs @@ -25,6 +25,7 @@ internal sealed class SimulationRuntimeShutdownWorkflow private readonly SimulationRuntimeResourceOwner _runtimeResources; private readonly SimulationRunControlWorkflow _simulationRunControlWorkflow; private readonly Action _recordDiagnostic; + private readonly Func _dispatch; private readonly BoundedShutdownCoordinator _shutdownCoordinator = new(); private int _shutdownRequested; @@ -33,7 +34,8 @@ internal SimulationRuntimeShutdownWorkflow( SimulationRuntimeLoop runtimeLoop, SimulationRuntimeResourceOwner runtimeResources, SimulationRunControlWorkflow simulationRunControlWorkflow, - Action recordDiagnostic) + Action recordDiagnostic, + Func dispatch) { _engine = engine ?? throw new ArgumentNullException(nameof(engine)); _runtimeLoop = runtimeLoop ?? throw new ArgumentNullException(nameof(runtimeLoop)); @@ -43,6 +45,7 @@ internal SimulationRuntimeShutdownWorkflow( ?? throw new ArgumentNullException(nameof(simulationRunControlWorkflow)); _recordDiagnostic = recordDiagnostic ?? throw new ArgumentNullException(nameof(recordDiagnostic)); + _dispatch = dispatch ?? throw new ArgumentNullException(nameof(dispatch)); } internal bool IsShutdownRequested => Volatile.Read(ref _shutdownRequested) != 0; @@ -235,10 +238,31 @@ private async Task CompleteDisposeAfterShutdownAsync( Trace.TraceError(exception.ToString()); } - CompleteDisposeIfSafe(completeApplicationDispose); + if (!TryCompleteRuntimeDisposeIfSafe()) + { + return; + } + + if (completeApplicationDispose is null) + { + return; + } + + try + { + // The shutdown task may complete on a worker continuation; the shell + // callback owns WPF-bound ViewModel disposal and must use the existing + // composition dispatch boundary. + await _dispatch(() => InvokeApplicationDispose(completeApplicationDispose)) + .ConfigureAwait(false); + } + catch (Exception exception) + { + Trace.TraceError(exception.ToString()); + } } - private void CompleteDisposeIfSafe(Action? completeApplicationDispose) + private bool TryCompleteRuntimeDisposeIfSafe() { var runtimeDisposed = _runtimeResources.IsDisposed; try @@ -252,15 +276,16 @@ private void CompleteDisposeIfSafe(Action? completeApplicationDispose) if (!runtimeDisposed) { - return; + return false; } _simulationRunControlWorkflow.Dispose(); - if (completeApplicationDispose is null) - { - return; - } + return true; + } + + private static void InvokeApplicationDispose(Action completeApplicationDispose) + { try { completeApplicationDispose(); @@ -271,6 +296,16 @@ private void CompleteDisposeIfSafe(Action? completeApplicationDispose) } } + private void CompleteDisposeIfSafe(Action? completeApplicationDispose) + { + if (!TryCompleteRuntimeDisposeIfSafe() || completeApplicationDispose is null) + { + return; + } + + InvokeApplicationDispose(completeApplicationDispose); + } + private void RecordDiagnostic( SimulationOperationalDiagnosticKind kind, SimulationLogSeverity severity, diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeSnapshotProjection.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeSnapshotProjection.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationRuntimeSnapshotProjection.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Runtime/SimulationRuntimeSnapshotProjection.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchArtifactStore.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchArtifactStore.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchArtifactStore.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchArtifactStore.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchParticipant.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchParticipant.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchParticipant.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchParticipant.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchPresentation.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchPresentation.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchPresentation.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchPresentation.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchRepetitionRunner.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchRepetitionRunner.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchRepetitionRunner.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchRepetitionRunner.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchViewModel.cs similarity index 91% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchViewModel.cs index c95a3da..6c574a8 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioBatchViewModel.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioBatchViewModel.cs @@ -336,7 +336,8 @@ private async Task RunScenarioBatchAsy var targetId = _workspace.ScenarioTargetId; if (string.IsNullOrWhiteSpace(targetId)) { - _setStatus(OpenVisionLanguageService.T("Simulation.ScenarioTargetRequired")); + await DispatchPresentationAsync(() => + _setStatus(OpenVisionLanguageService.T("Simulation.ScenarioTargetRequired"))); return new(SimulationScenarioBatchParticipantOutcome.Idle); } @@ -365,13 +366,18 @@ private async Task RunScenarioBatchAsy repetitionCount, BuildIdentity.Current); - _batchWasCanceled = false; - _batchCompletedRuns = 0; - _artifactStore.SetLatestBatchResult(null); - _resetUnifiedEvidence(); - SetBatchRunning(true); - _setStatus(OpenVisionLanguageService.T("Simulation.BatchStarted")); - _log($"Sequential batch started · {repetitionCount} run(s)"); + DeterministicSimulationRunResultPackage? acceptedBaseline = null; + await DispatchPresentationAsync(() => + { + acceptedBaseline = AcceptedBatchBaseline; + _batchWasCanceled = false; + _batchCompletedRuns = 0; + _artifactStore.SetLatestBatchResult(null); + _resetUnifiedEvidence(); + SetBatchRunning(true); + _setStatus(OpenVisionLanguageService.T("Simulation.BatchStarted")); + _log($"Sequential batch started · {repetitionCount} run(s)"); + }); try { @@ -394,35 +400,51 @@ private async Task RunScenarioBatchAsy await UpdateBatchProgressAsync(runIndex); return result; }, - AcceptedBatchBaseline, + acceptedBaseline, cancellationToken); - _artifactStore.SetLatestBatchResult(batchResult); - - _setStatus(batchResult.IsSuccess - ? OpenVisionLanguageService.T("Simulation.BatchPassedStatus") - : OpenVisionLanguageService.T("Simulation.BatchMismatchStatus")); - _log( - batchResult.IsSuccess - ? $"Sequential batch passed · {batchResult.CompletedRuns} run(s) · {ShortHash(batchResult.EvidenceHash)}" - : FormatBatchMismatchLog(batchResult.FirstMismatch)); - PersistBatchArtifacts(); + await DispatchPresentationAsync(() => + { + _artifactStore.SetLatestBatchResult(batchResult); + _setStatus(batchResult.IsSuccess + ? OpenVisionLanguageService.T("Simulation.BatchPassedStatus") + : OpenVisionLanguageService.T("Simulation.BatchMismatchStatus")); + _log( + batchResult.IsSuccess + ? $"Sequential batch passed · {batchResult.CompletedRuns} run(s) · {ShortHash(batchResult.EvidenceHash)}" + : FormatBatchMismatchLog(batchResult.FirstMismatch)); + PersistBatchArtifacts(); + }); return new(SimulationScenarioBatchParticipantOutcome.Completed, batchResult); } catch (OperationCanceledException exception) when (cancellationToken.IsCancellationRequested) { - _batchWasCanceled = true; - _setStatus(OpenVisionLanguageService.T("Simulation.BatchCanceled")); - _log($"Sequential batch canceled after {_batchCompletedRuns} completed run(s)"); + await DispatchPresentationAsync(() => + { + _batchWasCanceled = true; + _setStatus(OpenVisionLanguageService.T("Simulation.BatchCanceled")); + _log($"Sequential batch canceled after {_batchCompletedRuns} completed run(s)"); + }); return new(SimulationScenarioBatchParticipantOutcome.Cancelled, Exception: exception); } finally { - SetBatchRunning(false); - RaiseChanged(); + await DispatchPresentationAsync(() => + { + SetBatchRunning(false); + RaiseChanged(); + }); } } - private Task UpdateBatchProgressAsync(int completedRuns) => _dispatchToUi(() => + private Task DispatchPresentationAsync(Action action) => _dispatchToUi(() => + { + if (!_disposed) + { + action(); + } + }); + + private Task UpdateBatchProgressAsync(int completedRuns) => DispatchPresentationAsync(() => { _batchCompletedRuns = completedRuns; RaiseChanged(); diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioExecutionCoordinator.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioExecutionCoordinator.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioExecutionCoordinator.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioExecutionCoordinator.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationScenarioWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Scenarios/SimulationScenarioWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationSessionCloseWorkflow.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationSessionCloseWorkflow.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationSessionCloseWorkflow.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationSessionCloseWorkflow.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationSessionCoordinator.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationSessionCoordinator.cs similarity index 99% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationSessionCoordinator.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationSessionCoordinator.cs index 837809c..765d06e 100644 --- a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationSessionCoordinator.cs +++ b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationSessionCoordinator.cs @@ -133,7 +133,8 @@ internal void Start(SimulationSessionStartup startup) _runtimeLoop, _runtimeResources, _runControl, - startup.RecordShutdownDiagnostic); + startup.RecordShutdownDiagnostic, + startup.Dispatch); _closeWorkflow = new( startup.ResolveUnsavedChanges, startup.ObserveProjectSave, diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationWorkspaceViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationWorkspaceViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationWorkspaceViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Session/SimulationWorkspaceViewModel.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationCommandPresentationDispatcher.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Trace/SimulationCommandPresentationDispatcher.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationCommandPresentationDispatcher.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Trace/SimulationCommandPresentationDispatcher.cs diff --git a/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationCommandTraceViewModel.cs b/src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Trace/SimulationCommandTraceViewModel.cs similarity index 100% rename from src/OpenVisionLab.MachineStudio/ViewModel/Simulation/SimulationCommandTraceViewModel.cs rename to src/OpenVisionLab.MachineStudio/ViewModel/Simulation/Trace/SimulationCommandTraceViewModel.cs diff --git a/tests/OpenVisionLab.Machine.Core.Tests/OpenVisionLab.Machine.Core.Tests.csproj b/tests/OpenVisionLab.Machine.Core.Tests/OpenVisionLab.Machine.Core.Tests.csproj index 490db65..8b8a71e 100644 --- a/tests/OpenVisionLab.Machine.Core.Tests/OpenVisionLab.Machine.Core.Tests.csproj +++ b/tests/OpenVisionLab.Machine.Core.Tests/OpenVisionLab.Machine.Core.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/OpenVisionLab.Machine.Core.Tests/ProjectDocumentStoreTests.cs b/tests/OpenVisionLab.Machine.Core.Tests/ProjectDocumentStoreTests.cs index 8f4e808..d3c3fc5 100644 --- a/tests/OpenVisionLab.Machine.Core.Tests/ProjectDocumentStoreTests.cs +++ b/tests/OpenVisionLab.Machine.Core.Tests/ProjectDocumentStoreTests.cs @@ -5,6 +5,7 @@ using OpenVisionLab.Machine.Core.Models; using OpenVisionLab.Machine.Core.Projects; using OpenVisionLab.Machine.Core.Sequences; +using OpenVisionLab.Machine.Persistence.Projects; using Xunit; namespace OpenVisionLab.Machine.Core.Tests; @@ -12,6 +13,7 @@ namespace OpenVisionLab.Machine.Core.Tests; public class ProjectDocumentStoreTests { private readonly ProjectDocumentStore _store = new(); + private readonly ProjectDocumentFileStore _fileStore = new(); [Fact] public void Serialize_DoesNotChangeProjectMetadata() @@ -57,12 +59,12 @@ public async Task SaveAsync_ReplacesExistingFileAndRetainsPreviousVersion() var path = Path.Combine(directory, "machine.ovmachine"); try { - await _store.SaveAsync(new MachineProjectDocument { Name = "Before" }, path); + await _fileStore.SaveAsync(new MachineProjectDocument { Name = "Before" }, path); var previousJson = await File.ReadAllTextAsync(path); - await _store.SaveAsync(new MachineProjectDocument { Name = "After" }, path); + await _fileStore.SaveAsync(new MachineProjectDocument { Name = "After" }, path); - Assert.Equal("After", (await _store.LoadAsync(path)).Name); + Assert.Equal("After", (await _fileStore.LoadAsync(path)).Name); Assert.Equal(previousJson, await File.ReadAllTextAsync(path + ".bak")); Assert.Empty(Directory.EnumerateFiles(directory, ".*.tmp")); } @@ -484,7 +486,7 @@ public async Task LoadAsync_UnsupportedSchema_DoesNotModifySource() await File.WriteAllTextAsync(path, json); var exception = await Assert.ThrowsAsync( - () => _store.LoadAsync(path)); + () => _fileStore.LoadAsync(path)); Assert.Equal(ProjectDocumentLoadErrorCode.UnsupportedSchema, exception.ErrorCode); Assert.Equal(json, await File.ReadAllTextAsync(path)); @@ -503,8 +505,8 @@ public async Task SaveAsync_CommitFailurePreservesFilesAndCallerMetadata() var path = Path.Combine(directory, "machine.ovmachine"); try { - await _store.SaveAsync(new MachineProjectDocument { Name = "Before" }, path); - await _store.SaveAsync(new MachineProjectDocument { Name = "Committed" }, path); + await _fileStore.SaveAsync(new MachineProjectDocument { Name = "Before" }, path); + await _fileStore.SaveAsync(new MachineProjectDocument { Name = "Committed" }, path); var primaryBeforeFailure = await File.ReadAllBytesAsync(path); var backupBeforeFailure = await File.ReadAllBytesAsync(path + ".bak"); @@ -524,7 +526,7 @@ public async Task SaveAsync_CommitFailurePreservesFilesAndCallerMetadata() FileShare.None)) { var exception = await Record.ExceptionAsync( - () => _store.SaveAsync(failedDocument, path)); + () => _fileStore.SaveAsync(failedDocument, path)); Assert.NotNull(exception); Assert.True( @@ -551,7 +553,7 @@ public async Task SaveAsync_CancelledWritePreservesFilesAndCallerMetadata() var path = Path.Combine(directory, "machine.ovmachine"); try { - await _store.SaveAsync(new MachineProjectDocument { Name = "Before" }, path); + await _fileStore.SaveAsync(new MachineProjectDocument { Name = "Before" }, path); var primaryBeforeFailure = await File.ReadAllBytesAsync(path); var failedDocument = new MachineProjectDocument @@ -566,7 +568,7 @@ public async Task SaveAsync_CancelledWritePreservesFilesAndCallerMetadata() cancellation.Cancel(); await Assert.ThrowsAnyAsync( - () => _store.SaveAsync(failedDocument, path, cancellation.Token)); + () => _fileStore.SaveAsync(failedDocument, path, cancellation.Token)); Assert.Equal(primaryBeforeFailure, await File.ReadAllBytesAsync(path)); Assert.Equal(schemaBeforeFailure, failedDocument.Schema); @@ -592,7 +594,7 @@ public async Task LoadAsync_CorruptPrimaryLoadsValidBackupWithoutRewritingSource await File.WriteAllTextAsync(path, primaryJson); await File.WriteAllTextAsync(backupPath, backupJson); - var loaded = await _store.LoadAsync(path); + var loaded = await _fileStore.LoadAsync(path); Assert.Equal("Recovered", loaded.Name); Assert.Equal(primaryJson, await File.ReadAllTextAsync(path)); @@ -618,7 +620,7 @@ public async Task LoadAsync_UnsupportedPrimarySchemaDoesNotFallbackToBackup() await File.WriteAllTextAsync(backupPath, backupJson); var exception = await Assert.ThrowsAsync( - () => _store.LoadAsync(path)); + () => _fileStore.LoadAsync(path)); Assert.Equal(ProjectDocumentLoadErrorCode.UnsupportedSchema, exception.ErrorCode); Assert.Equal("2.0", exception.ProjectSchema); @@ -641,7 +643,7 @@ public async Task LoadAsync_MissingPrimaryLoadsValidBackup() var backupJson = _store.Save(new MachineProjectDocument { Name = "Recovered missing primary" }); await File.WriteAllTextAsync(path + ".bak", backupJson); - var loaded = await _store.LoadAsync(path); + var loaded = await _fileStore.LoadAsync(path); Assert.Equal("Recovered missing primary", loaded.Name); Assert.False(File.Exists(path)); @@ -678,7 +680,7 @@ public async Task LoadAsync_SemanticallyDamagedPrimaryLoadsValidBackup() await File.WriteAllTextAsync(path, primaryJson); await File.WriteAllTextAsync(path + ".bak", backupJson); - var loaded = await _store.LoadAsync(path); + var loaded = await _fileStore.LoadAsync(path); Assert.Equal("Recovered semantic damage", loaded.Name); Assert.Equal(primaryJson, await File.ReadAllTextAsync(path)); @@ -1103,7 +1105,7 @@ public void Load_UndefinedPlaceholderDecision_IsRejected() public async Task LoadAsync_SampleFile_ParsesWithoutError() { var path = "sample-pick-and-place.ovmachine"; - var loaded = await _store.LoadAsync(path); + var loaded = await _fileStore.LoadAsync(path); Assert.Equal("Sample Pick-and-Place Cell", loaded.Name); Assert.Equal(2, loaded.Axes.Count); @@ -1146,7 +1148,7 @@ public async Task LoadAsync_SampleFile_ParsesWithoutError() [Fact] public async Task LoadAsync_VisionInspectionCell_PreservesDigitalCycleContract() { - var loaded = await _store.LoadAsync("VisionInspectionCell.ovmachine"); + var loaded = await _fileStore.LoadAsync("VisionInspectionCell.ovmachine"); Assert.Equal("1.5", loaded.Schema); Assert.Equal(3, loaded.Channels.Count); @@ -1189,7 +1191,7 @@ public async Task LoadAsync_VisionInspectionCell_PreservesDigitalCycleContract() [Fact] public async Task LoadAsync_AutomaticTransferCell_PreservesLayoutSensorAndAutomaticRunContract() { - var loaded = await _store.LoadAsync("AutomaticTransferCell.ovmachine"); + var loaded = await _fileStore.LoadAsync("AutomaticTransferCell.ovmachine"); Assert.Equal("1.5", loaded.Schema); Assert.Equal("main-cell", loaded.Simulation.ActiveLayoutId); diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationHandoffRequestFactoryTests.cs b/tests/OpenVisionLab.Machine.Infrastructure.Tests/MachineIntegrationHandoffRequestFactoryTests.cs similarity index 73% rename from tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationHandoffRequestFactoryTests.cs rename to tests/OpenVisionLab.Machine.Infrastructure.Tests/MachineIntegrationHandoffRequestFactoryTests.cs index f61f908..fbea575 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationHandoffRequestFactoryTests.cs +++ b/tests/OpenVisionLab.Machine.Infrastructure.Tests/MachineIntegrationHandoffRequestFactoryTests.cs @@ -1,12 +1,9 @@ using System.Security.Cryptography; using OpenVisionLab.Integration.Contracts; using OpenVisionLab.Machine.Infrastructure.Integration; -using OpenVisionLab.Machine.Simulation.Camera; -using OpenVisionLab.Machine.Vision.Models; -using OpenVisionLab.MachineStudio.ViewModel; using Xunit; -namespace OpenVisionLab.MachineStudio.Tests; +namespace OpenVisionLab.Machine.Infrastructure.Tests; public sealed class MachineIntegrationHandoffRequestFactoryTests { @@ -19,14 +16,11 @@ public async Task CreateBuildsTwoDRequestAndProjectionFromMatchingSourceEvidence var factory = new MachineIntegrationHandoffRequestFactory(); var request = factory.Create(CreateInput( fixture, - new VirtualCameraFrameEvidence( + new MachineIntegrationFrameEvidence( "frame-1", "assets/input.raw", Convert.ToHexString(SHA256.HashData(content)), - content.LongLength, - 640, - 480, - "Mono8"))); + content.LongLength))); Assert.NotNull(request); var handoff = request!; @@ -50,14 +44,11 @@ public async Task CreateRejectsSourceWhenBytesNoLongerMatchFrameEvidence() using var fixture = new TemporaryProject(); var original = new byte[] { 0x01, 0x02, 0x03 }; await fixture.WriteAsync("input.raw", original); - var frame = new VirtualCameraFrameEvidence( + var frame = new MachineIntegrationFrameEvidence( "frame-1", "input.raw", Convert.ToHexString(SHA256.HashData(original)), - original.LongLength, - 3, - 1, - "Mono8"); + original.LongLength); await fixture.WriteAsync("input.raw", [0x01, 0x02, 0x04]); var request = new MachineIntegrationHandoffRequestFactory().Create( @@ -68,39 +59,39 @@ public async Task CreateRejectsSourceWhenBytesNoLongerMatchFrameEvidence() private static MachineIntegrationHandoffRequestInput CreateInput( TemporaryProject fixture, - VirtualCameraFrameEvidence frame, + MachineIntegrationFrameEvidence frame, string sourceRelativePath = "assets/input.raw", int width = 640, int height = 480) => new( - "machine-project", - "machine-project/1.0", - "sequence-001", - "inspect-1", - "camera-virtual", - "acquisition-1", - fixture.ProjectPath, - sourceRelativePath, - width, - height, - fixture.RecipePath, - frame, - new IntegrationApplicationIdentity( - IntegrationApplicationIds.MachineStudio, - "1.0.0", - new string('1', 40), - IntegrationSourceState.Clean), - new IntegrationApplicationIdentity( - IntegrationApplicationIds.TwoDStudio, - "2.0.0", - new string('2', 40), - IntegrationSourceState.Clean)); + "machine-project", + "machine-project/1.0", + "sequence-001", + "inspect-1", + "camera-virtual", + "acquisition-1", + fixture.ProjectPath, + sourceRelativePath, + width, + height, + fixture.RecipePath, + frame, + new IntegrationApplicationIdentity( + IntegrationApplicationIds.MachineStudio, + "1.0.0", + new string('1', 40), + IntegrationSourceState.Clean), + new IntegrationApplicationIdentity( + IntegrationApplicationIds.TwoDStudio, + "2.0.0", + new string('2', 40), + IntegrationSourceState.Clean)); private sealed class TemporaryProject : IDisposable { public TemporaryProject() { Root = Path.Combine( - "D:\\OpenVisionLab-TestData\\OpenVisionLab-Machine-Studio\\integration-handoff-factory-tests", + "D:\\OpenVisionLab-TestData\\Machine\\handoff-request-boundary-20260909\\factory-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(Root); ProjectPath = Path.Combine(Root, "machine.ovmachine"); diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/ServoAxisComponentTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Axis/ServoAxisComponentTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/ServoAxisComponentTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Axis/ServoAxisComponentTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/TrapezoidalMotionProfileTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Axis/TrapezoidalMotionProfileTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/TrapezoidalMotionProfileTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Axis/TrapezoidalMotionProfileTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicVirtualCameraTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Camera/DeterministicVirtualCameraTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicVirtualCameraTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Camera/DeterministicVirtualCameraTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/VirtualCameraInspectionTemplateTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Camera/VirtualCameraInspectionTemplateTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/VirtualCameraInspectionTemplateTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Camera/VirtualCameraInspectionTemplateTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationAutomaticRunCommandHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationAutomaticRunCommandHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationAutomaticRunCommandHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationAutomaticRunCommandHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioCommandHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScenarioCommandHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioCommandHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScenarioCommandHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioProgressHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScenarioProgressHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioProgressHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScenarioProgressHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioStopHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScenarioStopHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioStopHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScenarioStopHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScheduledFaultInjectionHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScheduledFaultInjectionHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScheduledFaultInjectionHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScheduledFaultInjectionHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScheduledFaultRecoveryHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScheduledFaultRecoveryHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScheduledFaultRecoveryHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationConditionScheduledFaultRecoveryHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationFaultCommandHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationFaultCommandHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationFaultCommandHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationFaultCommandHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationManualControlCommandHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationManualControlCommandHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationManualControlCommandHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationManualControlCommandHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRunControlCommandHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationRunControlCommandHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRunControlCommandHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationRunControlCommandHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationSequenceCommandHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationSequenceCommandHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationSequenceCommandHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commands/SimulationSequenceCommandHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MultiAxisCommissioningRecipeTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Commissioning/MultiAxisCommissioningRecipeTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MultiAxisCommissioningRecipeTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Commissioning/MultiAxisCommissioningRecipeTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/FixedStepDelayConverterTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/FixedStepDelayConverterTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/FixedStepDelayConverterTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/FixedStepDelayConverterTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutWaferHandlerRuntimeCompilerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/MachineLayoutWaferHandlerRuntimeCompilerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutWaferHandlerRuntimeCompilerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/MachineLayoutWaferHandlerRuntimeCompilerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineProjectRuntimeCompilerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/MachineProjectRuntimeCompilerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineProjectRuntimeCompilerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/MachineProjectRuntimeCompilerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineProjectRuntimeComponentCompilerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/MachineProjectRuntimeComponentCompilerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineProjectRuntimeComponentCompilerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/MachineProjectRuntimeComponentCompilerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/PrealignerCompilerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/PrealignerCompilerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/PrealignerCompilerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/PrealignerCompilerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/RepresentativeRecipeCheckpointTemplateTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/RepresentativeRecipeCheckpointTemplateTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/RepresentativeRecipeCheckpointTemplateTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/RepresentativeRecipeCheckpointTemplateTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SemiconductorProcessBlockComposerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/SemiconductorProcessBlockComposerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SemiconductorProcessBlockComposerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/SemiconductorProcessBlockComposerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SemiconductorStationSkeletonTemplateTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/SemiconductorStationSkeletonTemplateTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SemiconductorStationSkeletonTemplateTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/SemiconductorStationSkeletonTemplateTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SequenceCompilerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/SequenceCompilerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SequenceCompilerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Compilation/SequenceCompilerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/AtomicEvidenceFileTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/AtomicEvidenceFileTests.cs similarity index 83% rename from tests/OpenVisionLab.Machine.Simulation.Tests/AtomicEvidenceFileTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/AtomicEvidenceFileTests.cs index 76fb5fa..b0e7b4f 100644 --- a/tests/OpenVisionLab.Machine.Simulation.Tests/AtomicEvidenceFileTests.cs +++ b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/AtomicEvidenceFileTests.cs @@ -111,6 +111,30 @@ public async Task OverlappingWritesKeepIndependentTemporaryFilesAndCompletePaylo Assert.Equal(path, Assert.Single(Directory.GetFiles(Path.GetDirectoryName(path)!))); } + [Fact] + public async Task CanceledAsyncWritePreservesTargetAndCleansTemporaryFile() + { + var path = CreatePath(); + File.WriteAllText(path, "previous evidence"); + string? attemptedTemporaryPath = null; + using var cancellation = new CancellationTokenSource(); + + await Assert.ThrowsAsync(() => AtomicEvidenceFile.WriteAsync( + path, + async (temporaryPath, _) => + { + attemptedTemporaryPath = temporaryPath; + await File.WriteAllTextAsync(temporaryPath, "incomplete evidence"); + cancellation.Cancel(); + }, + cancellation.Token)); + + Assert.NotNull(attemptedTemporaryPath); + Assert.False(File.Exists(attemptedTemporaryPath)); + Assert.Equal("previous evidence", File.ReadAllText(path)); + Assert.Equal(path, Assert.Single(Directory.GetFiles(Path.GetDirectoryName(path)!))); + } + private static string CreatePath() { var directory = Path.Combine(TestStorage.RootPath, "atomic-evidence-file", Guid.NewGuid().ToString("N")); diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/ConcreteFaultRepeatComparisonTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/ConcreteFaultRepeatComparisonTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/ConcreteFaultRepeatComparisonTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/ConcreteFaultRepeatComparisonTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationEvidenceExchangePackageTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/DeterministicSimulationEvidenceExchangePackageTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationEvidenceExchangePackageTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/DeterministicSimulationEvidenceExchangePackageTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicUnifiedCommissioningEvidencePackageTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/DeterministicUnifiedCommissioningEvidencePackageTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicUnifiedCommissioningEvidencePackageTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/DeterministicUnifiedCommissioningEvidencePackageTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicVisionExecutionEvidencePackageTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/DeterministicVisionExecutionEvidencePackageTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicVisionExecutionEvidencePackageTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/DeterministicVisionExecutionEvidencePackageTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/PickAndPlaceFaultRecoveryEvidenceTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/PickAndPlaceFaultRecoveryEvidenceTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/PickAndPlaceFaultRecoveryEvidenceTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/PickAndPlaceFaultRecoveryEvidenceTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/ScaleEvidenceTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/ScaleEvidenceTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/ScaleEvidenceTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/ScaleEvidenceTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationEvidenceRetentionPolicyTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/SimulationEvidenceRetentionPolicyTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationEvidenceRetentionPolicyTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/SimulationEvidenceRetentionPolicyTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationOutputStoreTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/SimulationOutputStoreTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationOutputStoreTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/SimulationOutputStoreTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SustainedScaleEvidenceTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/SustainedScaleEvidenceTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SustainedScaleEvidenceTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Evidence/SustainedScaleEvidenceTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/AutomaticTransferCellScheduledFaultRecoveryTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/AutomaticTransferCellScheduledFaultRecoveryTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/AutomaticTransferCellScheduledFaultRecoveryTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/AutomaticTransferCellScheduledFaultRecoveryTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicConditionScenarioTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicConditionScenarioTests.cs similarity index 99% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicConditionScenarioTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicConditionScenarioTests.cs index 35b8541..aadc2f1 100644 --- a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicConditionScenarioTests.cs +++ b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicConditionScenarioTests.cs @@ -480,6 +480,7 @@ public void ProfileJson_RoundTripsAndNormalizesDefaults() DeterministicConditionScenarioProfile.LoadFromJson(path)); Assert.Equal(normalized, DeterministicConditionScenarioProfile.Normalize(loaded)); Assert.Empty(DeterministicConditionScenarioProfile.Validate(loaded)); + Assert.Equal(path, Assert.Single(Directory.GetFiles(Path.GetDirectoryName(path)!))); } finally { diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultInjectionTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultInjectionTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultInjectionTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultInjectionTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioHeadlessRunnerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioHeadlessRunnerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioHeadlessRunnerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioHeadlessRunnerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioProfileTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioProfileTests.cs similarity index 72% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioProfileTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioProfileTests.cs index c39a526..8df548d 100644 --- a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioProfileTests.cs +++ b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioProfileTests.cs @@ -1,5 +1,6 @@ using System.Linq; using OpenVisionLab.Machine.Simulation.FaultScenarios; +using OpenVisionLab.TestSupport; using Xunit; namespace OpenVisionLab.Machine.Simulation.Tests; @@ -92,4 +93,39 @@ public void Validate_ActionOutsideDuration_Fails() errors, error => error.Contains("targets tick 2", System.StringComparison.OrdinalIgnoreCase)); } + + [Fact] + public void SaveToJson_WritesNormalizedProfileWithoutTemporaryFiles() + { + var directory = Path.Combine( + TestStorage.RootPath, + "Simulation", + "FaultScenarioProfileTests", + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, "scenario.json"); + var scenario = new DeterministicFaultScenarioProfile( + SchemaVersion: 1, + ScenarioId: " persisted-fault ", + Name: "Persisted fault", + Description: "Atomic profile fixture.", + DurationTicks: 2, + Actions: []); + + try + { + DeterministicFaultScenarioProfile.SaveToJson(scenario, path); + + var loaded = Assert.IsType( + DeterministicFaultScenarioProfile.LoadFromJson(path)); + Assert.Equal( + DeterministicFaultScenarioProfile.Normalize(scenario), + DeterministicFaultScenarioProfile.Normalize(loaded)); + Assert.Equal(path, Assert.Single(Directory.GetFiles(directory))); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } } diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioReplayTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioReplayTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicFaultScenarioReplayTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/DeterministicFaultScenarioReplayTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioRuntimeTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/SimulationConditionScenarioRuntimeTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationConditionScenarioRuntimeTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/SimulationConditionScenarioRuntimeTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationFaultTargetCatalogTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Faults/SimulationFaultTargetCatalogTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationFaultTargetCatalogTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Faults/SimulationFaultTargetCatalogTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/AutomaticRunIntegrationTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/AutomaticRunIntegrationTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/AutomaticRunIntegrationTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/AutomaticRunIntegrationTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/AutomaticTransferCellEndToEndTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/AutomaticTransferCellEndToEndTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/AutomaticTransferCellEndToEndTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/AutomaticTransferCellEndToEndTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutAutomaticRunIntegrationTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/MachineLayoutAutomaticRunIntegrationTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutAutomaticRunIntegrationTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/MachineLayoutAutomaticRunIntegrationTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/PickAndPlaceSampleEndToEndTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/PickAndPlaceSampleEndToEndTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/PickAndPlaceSampleEndToEndTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/PickAndPlaceSampleEndToEndTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SemiconductorRecipeEndToEndTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/SemiconductorRecipeEndToEndTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SemiconductorRecipeEndToEndTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/SemiconductorRecipeEndToEndTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SubsequenceCompositionTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/SubsequenceCompositionTests.cs similarity index 88% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SubsequenceCompositionTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/SubsequenceCompositionTests.cs index 07f25a0..9d9acd2 100644 --- a/tests/OpenVisionLab.Machine.Simulation.Tests/SubsequenceCompositionTests.cs +++ b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/SubsequenceCompositionTests.cs @@ -117,6 +117,41 @@ public void Executor_RoutesUnhandledChildFailureThroughCallerErrorStep() Assert.Null(routed.Snapshot.ActiveSequenceId); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Executor_WatchdogCannotRecoverThroughCallerErrorStep(bool childWatchdog) + { + var childDefinition = ChildTimeoutDefinition(); + childDefinition.Steps[0].TimeoutMs = 0; + childDefinition.WatchdogTimeoutMs = childWatchdog ? 5 : 0; + var parentDefinition = ParentWithErrorDefinition(); + parentDefinition.WatchdogTimeoutMs = childWatchdog ? 0 : 10; + var child = Compile(childDefinition, "child", "parent", "child"); + var parent = Compile(parentDefinition, "parent", "parent", "child"); + var executor = new DeterministicSequenceExecutor(parent, + new Dictionary(StringComparer.Ordinal) + { + [parent.Id] = parent, + [child.Id] = child + }); + + executor.Start(); + executor.Tick(TimeSpan.FromMilliseconds(5), new TestContext()); + var faulted = executor.Tick(TimeSpan.FromMilliseconds(5), new TestContext()); + + Assert.Equal(SequenceExecutionStatus.Faulted, faulted.Snapshot.Status); + Assert.False(faulted.Transitioned); + Assert.Equal("wait", faulted.Snapshot.CurrentStepId); + Assert.Equal(new[] { "parent", "child" }, faulted.Snapshot.CallStack); + Assert.Equal(SequenceExecutionErrorCode.SequenceWatchdogTimedOut, faulted.Error!.Code); + Assert.Equal(childWatchdog ? "child" : "parent", faulted.Error.SequenceId); + Assert.Equal(TimeSpan.FromMilliseconds(10), faulted.Snapshot.TotalElapsed); + Assert.Equal(SequenceExecutionStatus.Faulted, + executor.Tick(TimeSpan.FromMilliseconds(5), new TestContext()).Snapshot.Status); + Assert.Equal(TimeSpan.Zero, executor.Retry().Snapshot.TotalElapsed); + } + [Fact] public void Executor_RetryClearsNestedFaultBoundaryAndStartsRootFresh() { diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/VirtualCameraSequenceIntegrationTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/VirtualCameraSequenceIntegrationTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/VirtualCameraSequenceIntegrationTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/VirtualCameraSequenceIntegrationTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/VirtualIoSequenceIntegrationTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Integration/VirtualIoSequenceIntegrationTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/VirtualIoSequenceIntegrationTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Integration/VirtualIoSequenceIntegrationTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicConveyorWorkpieceTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicConveyorWorkpieceTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicConveyorWorkpieceTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicConveyorWorkpieceTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicInspectionHandoffTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicInspectionHandoffTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicInspectionHandoffTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicInspectionHandoffTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicInspectionSortRouterTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicInspectionSortRouterTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicInspectionSortRouterTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicInspectionSortRouterTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicLoadLockTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicLoadLockTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicLoadLockTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicLoadLockTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicMachineLayoutTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicMachineLayoutTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicMachineLayoutTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicMachineLayoutTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicOhtHandoffTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicOhtHandoffTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicOhtHandoffTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicOhtHandoffTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicPneumaticCylinderTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicPneumaticCylinderTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicPneumaticCylinderTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicPneumaticCylinderTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicPrealignerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicPrealignerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicPrealignerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicPrealignerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicWaferHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicWaferHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicWaferHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DeterministicWaferHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DigitalSensorManualForceTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DigitalSensorManualForceTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DigitalSensorManualForceTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/DigitalSensorManualForceTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutRuntimeConfigurationValidatorTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/MachineLayoutRuntimeConfigurationValidatorTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutRuntimeConfigurationValidatorTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/MachineLayoutRuntimeConfigurationValidatorTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutRuntimeResetterTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/MachineLayoutRuntimeResetterTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutRuntimeResetterTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/MachineLayoutRuntimeResetterTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutSignalBindingValidatorTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Layout/MachineLayoutSignalBindingValidatorTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/MachineLayoutSignalBindingValidatorTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Layout/MachineLayoutSignalBindingValidatorTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicRecipeDryRunRunnerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicRecipeDryRunRunnerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicRecipeDryRunRunnerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicRecipeDryRunRunnerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicScenarioAssertionTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicScenarioAssertionTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicScenarioAssertionTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicScenarioAssertionTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSignalHubTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSignalHubTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSignalHubTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSignalHubTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationBatchResultPackageTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationBatchResultPackageTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationBatchResultPackageTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationBatchResultPackageTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationCommandTraceStoreTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationCommandTraceStoreTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationCommandTraceStoreTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationCommandTraceStoreTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationCommandTraceTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationCommandTraceTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationCommandTraceTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationCommandTraceTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationRunResultPackageTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationRunResultPackageTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSimulationRunResultPackageTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/DeterministicSimulationRunResultPackageTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/FixedStepSimulationEngineTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/FixedStepSimulationEngineTests.cs similarity index 95% rename from tests/OpenVisionLab.Machine.Simulation.Tests/FixedStepSimulationEngineTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/FixedStepSimulationEngineTests.cs index dfabcb3..bbd6983 100644 --- a/tests/OpenVisionLab.Machine.Simulation.Tests/FixedStepSimulationEngineTests.cs +++ b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/FixedStepSimulationEngineTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using OpenVisionLab.Machine.Core.Channels; using OpenVisionLab.Machine.Core.Sequences; using OpenVisionLab.Machine.Sequence.Compilation; @@ -5,6 +6,7 @@ using OpenVisionLab.Machine.Simulation.Commands; using OpenVisionLab.Machine.Simulation.Engine; using OpenVisionLab.Machine.Simulation.Events; +using OpenVisionLab.Machine.Simulation.Scenarios; using OpenVisionLab.Machine.Simulation.Snapshots; using Xunit; @@ -462,6 +464,53 @@ public async Task Deterministic_SameInput_SameFinalPosition() Assert.Equal(100, results[0], 6); } + [Fact] + public async Task StepReplay_IsIndependentOfCallerCulture() + { + async Task<(SimulationSnapshot Snapshot, string EventHash)> ReplayAsync(string cultureName) + { + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + using var engine = new FixedStepSimulationEngine(new SimulationSettings { FixedStep = TimeSpan.FromMilliseconds(5) }); + var axis = new ServoAxisComponent(CreateAxisConfig()); + Assert.True(axis.MoveAbsolute(1.25).IsAccepted); + engine.AddAxis(axis); + await engine.StartAsync(); + for (var tick = 0; tick < 100; tick++) + { + Assert.True((await engine.EnqueueCommandAsync(new StepCommand())).IsAccepted); + } + + await engine.StopAsync(); + Assert.Equal(cultureName, CultureInfo.CurrentCulture.Name); + var events = await ReadAllEventsAsync(engine); + Assert.Contains(events, item => item.Code == "AxisTargetReached"); + return (engine.CurrentSnapshot, DeterministicSimulationRunEvidenceHasher.HashEvents(events)); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } + + var first = await ReplayAsync("fr-FR"); + var second = await ReplayAsync("ko-KR"); + Assert.Equal(100, first.Snapshot.TickIndex); + Assert.Equal(first.Snapshot.TickIndex, second.Snapshot.TickIndex); + Assert.Equal(first.Snapshot.SimulationTime, second.Snapshot.SimulationTime); + var firstAxis = Assert.Single(first.Snapshot.Axes); + var secondAxis = Assert.Single(second.Snapshot.Axes); + Assert.Equal(1.25, firstAxis.Position); + Assert.Equal(firstAxis.Position, secondAxis.Position); + Assert.Equal(firstAxis.Velocity, secondAxis.Velocity); + Assert.Equal(firstAxis.CommandPosition, secondAxis.CommandPosition); + Assert.Equal(firstAxis.State, secondAxis.State); + Assert.Equal(firstAxis.DriveAlarmActive, secondAxis.DriveAlarmActive); + Assert.Equal(first.EventHash, second.EventHash); + } + [Fact] public async Task ManualControl_JogStopHomeAndResetUseOrderedEngineState() { diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/RuntimeGenerationTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/RuntimeGenerationTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/RuntimeGenerationTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/RuntimeGenerationTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationAutomaticRunCycleHandlerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationAutomaticRunCycleHandlerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationAutomaticRunCycleHandlerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationAutomaticRunCycleHandlerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationAutomaticRunRuntimeTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationAutomaticRunRuntimeTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationAutomaticRunRuntimeTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationAutomaticRunRuntimeTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationFaultRuntimeTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationFaultRuntimeTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationFaultRuntimeTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationFaultRuntimeTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationPhysicalRuntimeTickTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationPhysicalRuntimeTickTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationPhysicalRuntimeTickTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationPhysicalRuntimeTickTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRunLoopTimingTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationRunLoopTimingTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRunLoopTimingTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationRunLoopTimingTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRuntimeConfigurationBuilderTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationRuntimeConfigurationBuilderTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRuntimeConfigurationBuilderTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationRuntimeConfigurationBuilderTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRuntimeStateTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationRuntimeStateTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationRuntimeStateTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationRuntimeStateTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationSnapshotFactoryTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationSnapshotFactoryTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationSnapshotFactoryTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Runtime/SimulationSnapshotFactoryTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceDebugStateTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceDebugStateTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceDebugStateTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceDebugStateTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceExecutorTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceExecutorTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceExecutorTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceExecutorTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceRuntimeContextTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceRuntimeContextTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceRuntimeContextTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceRuntimeContextTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceStepPreviewRunnerTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceStepPreviewRunnerTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/DeterministicSequenceStepPreviewRunnerTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/DeterministicSequenceStepPreviewRunnerTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SequenceDefinitionEditorTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SequenceDefinitionEditorTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SequenceDefinitionEditorTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SequenceDefinitionEditorTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SequenceStepNormalizationTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SequenceStepNormalizationTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SequenceStepNormalizationTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SequenceStepNormalizationTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SequenceStepTemplateCatalogTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SequenceStepTemplateCatalogTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SequenceStepTemplateCatalogTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SequenceStepTemplateCatalogTests.cs diff --git a/tests/OpenVisionLab.Machine.Simulation.Tests/SimulationSequenceRuntimeTests.cs b/tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SimulationSequenceRuntimeTests.cs similarity index 100% rename from tests/OpenVisionLab.Machine.Simulation.Tests/SimulationSequenceRuntimeTests.cs rename to tests/OpenVisionLab.Machine.Simulation.Tests/Sequence/SimulationSequenceRuntimeTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Camera/VirtualCameraFirstUseViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Camera/VirtualCameraFirstUseViewModelTests.cs index 35d6d56..e9177e2 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Camera/VirtualCameraFirstUseViewModelTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Camera/VirtualCameraFirstUseViewModelTests.cs @@ -1,6 +1,7 @@ using OpenVisionLab.Machine.Core.Devices; using OpenVisionLab.Machine.Core.Layouts; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.Machine.Core.Sequences; using OpenVisionLab.Machine.Simulation.Camera; using OpenVisionLab.MachineStudio.ViewModel; @@ -76,7 +77,7 @@ public async Task SaveAndReopenRestoresCameraRecipeAndBranchesWithoutAcquisition try { - var store = new ProjectDocumentStore(); + var store = new ProjectDocumentFileStore(); string authoredCameraId; string authoredSequenceId; using (var authoringViewModel = new MainViewModel( diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Commissioning/MultiAxisCommissioningViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Commissioning/MultiAxisCommissioningViewModelTests.cs index 8f0e422..53fb66a 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Commissioning/MultiAxisCommissioningViewModelTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Commissioning/MultiAxisCommissioningViewModelTests.cs @@ -1,6 +1,7 @@ using OpenVisionLab; using OpenVisionLab.Machine.Core.Axes; using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Simulation.Axis; using OpenVisionLab.Machine.Simulation.Commissioning; using OpenVisionLab.Machine.Simulation.Compilation; using OpenVisionLab.Machine.Simulation.Engine; @@ -71,6 +72,100 @@ public void RecipeChangeInvalidatesOnlyTheOwnedValidationContext() Assert.True(viewModel.CanValidate); } + [Fact] + public async Task ValidationCompletionUsesTheUiDispatchBoundary() + { + OpenVisionLanguageService.Load(); + var project = CreateProject(); + project.MultiAxisCommissioningRecipe!.ValidationRepetitions = 2; + var recipe = new MultiAxisCommissioningRecipeEditorViewModel(() => { }); + recipe.Load(project); + var dispatchScope = false; + var statusCount = 0; + var completionStatusWasDispatched = false; + var completionLogWasDispatched = false; + using var viewModel = new MultiAxisCommissioningViewModel( + recipe, + () => true, + () => false, + () => project, + () => null, + () => "{}", + () => new SimulationRuntimeConfiguration( + [ + new AxisConfiguration + { + Id = "x", + Name = "X", + MinimumPosition = 0, + MaximumPosition = 300, + HomePosition = 0, + MaximumVelocity = 100, + Acceleration = 100, + Deceleration = 100, + FollowingErrorLimit = 10 + }, + new AxisConfiguration + { + Id = "y", + Name = "Y", + MinimumPosition = 0, + MaximumPosition = 300, + HomePosition = 0, + MaximumVelocity = 100, + Acceleration = 100, + Deceleration = 100, + FollowingErrorLimit = 10 + } + ], + [], + []), + TimeSpan.FromMilliseconds(5), + action => + { + var previousScope = dispatchScope; + dispatchScope = true; + try + { + action(); + } + finally + { + dispatchScope = previousScope; + } + + return Task.CompletedTask; + }, + _ => + { + statusCount++; + if (statusCount >= 2) + { + completionStatusWasDispatched |= dispatchScope; + } + }, + _ => + { + if (statusCount >= 2) + { + completionLogWasDispatched |= dispatchScope; + } + }, + _ => { }, + _ => { }, + _ => { }); + + viewModel.ValidateCommand.Execute(null); + var validationTask = viewModel.ValidationTask; + Assert.NotNull(validationTask); + await validationTask!.WaitAsync(TimeSpan.FromSeconds(10)); + + Assert.NotNull(viewModel.LatestResult); + Assert.True(completionStatusWasDispatched); + Assert.True(completionLogWasDispatched); + Assert.False(viewModel.IsValidationRunning); + } + private static MultiAxisCommissioningViewModel CreateViewModel( MachineProjectDocument project, MultiAxisCommissioningRecipeEditorViewModel recipe, diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Hosting/MainWpfInteractionHostTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Hosting/MainWpfInteractionHostTests.cs new file mode 100644 index 0000000..a695118 --- /dev/null +++ b/tests/OpenVisionLab.MachineStudio.Tests/Hosting/MainWpfInteractionHostTests.cs @@ -0,0 +1,52 @@ +using System.Windows.Threading; +using OpenVisionLab.MachineStudio.View.Shell; +using Xunit; + +namespace OpenVisionLab.MachineStudio.Tests; + +public sealed class MainWpfInteractionHostTests +{ + [Fact] + public async Task DropsUiCallbacksAfterDispatcherShutdownBegins() + { + var executed = await RunOnStaAsync(() => + { + var dispatcher = Dispatcher.CurrentDispatcher; + var host = new MainWpfInteractionHost(dispatcher); + var callbackCount = 0; + + dispatcher.InvokeShutdown(); + + var dispatchTask = host.DispatchOnUiThreadAsync(() => + { + callbackCount++; + return Task.CompletedTask; + }); + var progressTask = host.DispatchBatchProgressAsync(() => callbackCount++); + Task.WhenAll(dispatchTask, progressTask).GetAwaiter().GetResult(); + return callbackCount; + }); + + Assert.Equal(0, executed); + } + + private static Task RunOnStaAsync(Func action) + { + var completion = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + var thread = new Thread(() => + { + try + { + completion.SetResult(action()); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + return completion.Task; + } +} diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationPathReadinessPolicyTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationPathReadinessPolicyTests.cs index dbff4dd..4eba6a9 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationPathReadinessPolicyTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationPathReadinessPolicyTests.cs @@ -1,4 +1,5 @@ using OpenVisionLab.TestSupport; +using OpenVisionLab.Machine.Infrastructure.Integration; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationResultObservationWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationResultObservationWorkflowTests.cs index 5112fda..c836c17 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationResultObservationWorkflowTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationResultObservationWorkflowTests.cs @@ -52,6 +52,53 @@ public async Task ResultWatcherSchedulesInjectedRefreshWithoutViewModel() Assert.Null(watcherException); } + [Fact] + public async Task ResultWatcherExceptionUsesInjectedUiDispatchBoundary() + { + using var fixture = new TestRoot(); + var dispatchDepth = 0; + var exceptionTcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + using var workflow = new MachineIntegrationResultObservationWorkflow( + () => fixture.ExchangeRoot, + () => "project-1", + () => true, + () => false, + () => throw new IOException("watcher-failure"), + async operation => + { + Interlocked.Increment(ref dispatchDepth); + try + { + await operation(); + } + finally + { + Interlocked.Decrement(ref dispatchDepth); + } + }, + exception => + { + Assert.Equal(1, Volatile.Read(ref dispatchDepth)); + exceptionTcs.TrySetResult(exception); + }); + + workflow.ConfigureWatcher(); + var transactionDirectory = Path.Combine( + fixture.ExchangeRoot, + IntegrationTransactionLayout.TransactionsDirectoryName, + Guid.NewGuid().ToString("D")); + Directory.CreateDirectory(transactionDirectory); + File.WriteAllText( + Path.Combine(transactionDirectory, IntegrationTransactionLayout.ResultFileName), + "{}"); + + var exception = await exceptionTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + Assert.IsType(exception); + Assert.Equal(0, Volatile.Read(ref dispatchDepth)); + } + [Fact] public async Task DisposingWorkflowCancelsPendingResultRefresh() { diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationSetupStoreTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationSetupStoreTests.cs index 62758c6..5fececd 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationSetupStoreTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationSetupStoreTests.cs @@ -1,4 +1,5 @@ using System.Net; +using OpenVisionLab.Machine.Infrastructure.Integration; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; @@ -26,6 +27,7 @@ public void SaveAndLoadPreserveSetupWithoutViewModel() store.Save(setup); var loaded = store.Load(); + Assert.True(Directory.Exists(fixture.ExchangeRoot)); Assert.Equal(MachineIntegrationSetupLoadWarning.None, loaded.Warning); Assert.Null(loaded.ErrorMessage); Assert.Equal(setup, loaded.Settings); diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpControlViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpControlViewModelTests.cs index 842631e..e86b60e 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpControlViewModelTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpControlViewModelTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Net; using System.Security.Cryptography; using System.Text; @@ -28,6 +29,46 @@ public async Task ListenerLifecycleRemainsOwnedByTcpControlViewModel() Assert.True(viewModel.CanEditTcpSetup); } + [Fact] + public async Task ListenerPresentationUsesTheInjectedUiDispatchBoundary() + { + using var fixture = new TestRoot(); + var dispatchThreads = new ConcurrentBag(); + var propertyChangedThreads = new ConcurrentBag(); + using var viewModel = new MachineIntegrationTcpControlViewModel( + () => new MachineIntegrationTcpSettings( + fixture.ExchangeRoot, + IPAddress.Loopback, + 0, + IPAddress.Loopback.ToString(), + 45101), + () => null, + () => Task.CompletedTask, + _ => { }, + operation => Task.Run(async () => + { + dispatchThreads.Add(Environment.CurrentManagedThreadId); + await operation(); + })); + viewModel.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(viewModel.IsTcpListening)) + { + propertyChangedThreads.Add(Environment.CurrentManagedThreadId); + } + }; + + viewModel.SetSessionSharedKey(CreateEncodedKey("tcp-control-dispatch")); + await viewModel.StartTcpListenerAsync(); + await viewModel.StopTcpListenerAsync(); + + Assert.True(dispatchThreads.Count >= 2); + Assert.NotEmpty(propertyChangedThreads); + Assert.All( + propertyChangedThreads, + threadId => Assert.Contains(threadId, dispatchThreads)); + } + [Fact] public async Task InvalidSettingsArePresentedWithoutStartingTransport() { diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpOperationOwnerTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpOperationOwnerTests.cs index 785ffa7..f654eae 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpOperationOwnerTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationTcpOperationOwnerTests.cs @@ -1,4 +1,5 @@ using System.Net; +using OpenVisionLab.Machine.Infrastructure.Integration; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationViewModelTests.cs index 7f65b1d..15db2ed 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationViewModelTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Integration/MachineIntegrationViewModelTests.cs @@ -34,10 +34,10 @@ public async Task SourceOwnerUsesLatestContextForAdmissionAndExplicitPublication using var fixture = new IntegrationFixture(); var context = fixture.CreateContext(); using var viewModel = new MachineIntegrationViewModel(() => context, fixture.CreateProducer, () => context.ProjectId, fixture.SettingsPath); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = "2.1.0"; - viewModel.TwoDConsumerCommit = new string('2', 40); + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = "2.1.0"; + viewModel.Setup.TwoDConsumerCommit = new string('2', 40); viewModel.RefreshSourceContext(); Assert.True(viewModel.CanPublishTwoDImageHandoff); @@ -105,10 +105,10 @@ public void SourceOwnerCloseAndDisposeBlockRequestsWithoutReadingContext() () => { reads++; return fixture.CreateContext(); }, fixture.CreateProducer, () => "project-1", fixture.SettingsPath); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = "2.1.0"; - viewModel.TwoDConsumerCommit = new string('2', 40); + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = "2.1.0"; + viewModel.Setup.TwoDConsumerCommit = new string('2', 40); Assert.True(viewModel.CanPublishTwoDImageHandoff); viewModel.SetSessionCloseAdmission(true); @@ -135,18 +135,18 @@ public async Task SetupRoundTripAndExplicitRefreshDoNotRunInspection() Assert.Contains("No folder was scanned", viewModel.StatusText); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = "2.1.0"; - viewModel.TwoDConsumerCommit = new string('2', 40); - viewModel.TcpListenAddress = IPAddress.Loopback.ToString(); - viewModel.TcpListenPortText = "45111"; - viewModel.TcpPeerHost = IPAddress.Loopback.ToString(); - viewModel.TcpPeerPortText = "45112"; + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = "2.1.0"; + viewModel.Setup.TwoDConsumerCommit = new string('2', 40); + viewModel.Setup.TcpListenAddress = IPAddress.Loopback.ToString(); + viewModel.Setup.TcpListenPortText = "45111"; + viewModel.Setup.TcpPeerHost = IPAddress.Loopback.ToString(); + viewModel.Setup.TcpPeerPortText = "45112"; var encodedKey = Convert.ToBase64String( SHA256.HashData(Encoding.UTF8.GetBytes("setup-key"))); viewModel.SetSessionSharedKey(encodedKey); - viewModel.SaveSetupCommand.Execute(null); + viewModel.Setup.SaveSetupCommand.Execute(null); Assert.DoesNotContain( encodedKey, @@ -157,14 +157,14 @@ public async Task SetupRoundTripAndExplicitRefreshDoNotRunInspection() (_, _) => null, (_, _) => false); - Assert.Equal(fixture.ExchangeRoot, reloaded.ExchangeRoot); - Assert.Equal(fixture.RecipePath, reloaded.InspectionRecipePath); - Assert.Equal("2.1.0", reloaded.TwoDConsumerVersion); - Assert.Equal(new string('2', 40), reloaded.TwoDConsumerCommit); - Assert.Equal(IPAddress.Loopback.ToString(), reloaded.TcpListenAddress); - Assert.Equal("45111", reloaded.TcpListenPortText); - Assert.Equal(IPAddress.Loopback.ToString(), reloaded.TcpPeerHost); - Assert.Equal("45112", reloaded.TcpPeerPortText); + Assert.Equal(fixture.ExchangeRoot, reloaded.Setup.ExchangeRoot); + Assert.Equal(fixture.RecipePath, reloaded.Setup.InspectionRecipePath); + Assert.Equal("2.1.0", reloaded.Setup.TwoDConsumerVersion); + Assert.Equal(new string('2', 40), reloaded.Setup.TwoDConsumerCommit); + Assert.Equal(IPAddress.Loopback.ToString(), reloaded.Setup.TcpListenAddress); + Assert.Equal("45111", reloaded.Setup.TcpListenPortText); + Assert.Equal(IPAddress.Loopback.ToString(), reloaded.Setup.TcpPeerHost); + Assert.Equal("45112", reloaded.Setup.TcpPeerPortText); Assert.True(reloaded.CanRefreshResults); reloaded.RefreshResultsCommand.Execute(null); @@ -173,16 +173,16 @@ public async Task SetupRoundTripAndExplicitRefreshDoNotRunInspection() Assert.Empty(MachineIntegrationExchange.DiscoverTransactions(fixture.ExchangeRoot)); Assert.Contains("No inspection", reloaded.StatusText); - reloaded.ResetSetupCommand.Execute(null); + reloaded.Setup.ResetSetupCommand.Execute(null); - Assert.Equal(string.Empty, reloaded.ExchangeRoot); - Assert.Equal(string.Empty, reloaded.InspectionRecipePath); - Assert.Equal(string.Empty, reloaded.TwoDConsumerVersion); - Assert.Equal(string.Empty, reloaded.TwoDConsumerCommit); - Assert.Equal("127.0.0.1", reloaded.TcpListenAddress); - Assert.Equal("45101", reloaded.TcpListenPortText); - Assert.Equal("127.0.0.1", reloaded.TcpPeerHost); - Assert.Equal("45102", reloaded.TcpPeerPortText); + Assert.Equal(string.Empty, reloaded.Setup.ExchangeRoot); + Assert.Equal(string.Empty, reloaded.Setup.InspectionRecipePath); + Assert.Equal(string.Empty, reloaded.Setup.TwoDConsumerVersion); + Assert.Equal(string.Empty, reloaded.Setup.TwoDConsumerCommit); + Assert.Equal("127.0.0.1", reloaded.Setup.TcpListenAddress); + Assert.Equal("45101", reloaded.Setup.TcpListenPortText); + Assert.Equal("127.0.0.1", reloaded.Setup.TcpPeerHost); + Assert.Equal("45102", reloaded.Setup.TcpPeerPortText); } [Fact] @@ -211,26 +211,26 @@ public void BrowseCommandsUseInjectedSelectorsAndPreserveCancellation() return selectedRecipePath; }); - viewModel.BrowseExchangeRootCommand.Execute(null); - viewModel.BrowseRecipeCommand.Execute(null); + viewModel.Setup.BrowseExchangeRootCommand.Execute(null); + viewModel.Setup.BrowseRecipeCommand.Execute(null); Assert.Equal(1, exchangeSelectorCalls); Assert.Equal(1, recipeSelectorCalls); - Assert.Equal(selectedExchangeRoot, viewModel.ExchangeRoot); - Assert.Equal(selectedRecipePath, viewModel.InspectionRecipePath); + Assert.Equal(selectedExchangeRoot, viewModel.Setup.ExchangeRoot); + Assert.Equal(selectedRecipePath, viewModel.Setup.InspectionRecipePath); using var cancelledViewModel = fixture.CreateViewModel( (_, _) => null, (_, _) => false, _ => null, _ => null); - cancelledViewModel.ExchangeRoot = fixture.ExchangeRoot; - cancelledViewModel.InspectionRecipePath = fixture.RecipePath; - cancelledViewModel.BrowseExchangeRootCommand.Execute(null); - cancelledViewModel.BrowseRecipeCommand.Execute(null); + cancelledViewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + cancelledViewModel.Setup.InspectionRecipePath = fixture.RecipePath; + cancelledViewModel.Setup.BrowseExchangeRootCommand.Execute(null); + cancelledViewModel.Setup.BrowseRecipeCommand.Execute(null); - Assert.Equal(fixture.ExchangeRoot, cancelledViewModel.ExchangeRoot); - Assert.Equal(fixture.RecipePath, cancelledViewModel.InspectionRecipePath); + Assert.Equal(fixture.ExchangeRoot, cancelledViewModel.Setup.ExchangeRoot); + Assert.Equal(fixture.RecipePath, cancelledViewModel.Setup.InspectionRecipePath); } [Fact] @@ -241,10 +241,10 @@ public void DisposedIntegrationCommandsCannotExecute() (_, _) => null, (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = "2.1.0"; - viewModel.TwoDConsumerCommit = new string('2', 40); + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = "2.1.0"; + viewModel.Setup.TwoDConsumerCommit = new string('2', 40); Assert.True(viewModel.CanPublishTwoDImageHandoff); Assert.True(viewModel.CanRefreshResults); @@ -265,10 +265,10 @@ public void SessionCloseAdmissionBlocksFileAndTcpCommandsUntilReleased() (_, _) => null, (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = "2.1.0"; - viewModel.TwoDConsumerCommit = new string('2', 40); + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = "2.1.0"; + viewModel.Setup.TwoDConsumerCommit = new string('2', 40); Assert.True(viewModel.CanPublishTwoDImageHandoff); Assert.True(viewModel.CanRefreshResults); @@ -313,10 +313,10 @@ public async Task DisposeSuppressesLatePublishStatus() consumer); }, (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = "2.1.0"; - viewModel.TwoDConsumerCommit = new string('2', 40); + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = "2.1.0"; + viewModel.Setup.TwoDConsumerCommit = new string('2', 40); viewModel.PropertyChanged += (_, args) => { if (Volatile.Read(ref releaseRequest) == 2 @@ -363,10 +363,10 @@ public async Task ObserveAsyncWaitsForAnInFlightPublishOperation() return fixture.CreateRequest(recipePath, producer, requestedConsumer); }, (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = consumer.ApplicationVersion; - viewModel.TwoDConsumerCommit = consumer.SourceCommit; + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = consumer.ApplicationVersion; + viewModel.Setup.TwoDConsumerCommit = consumer.SourceCommit; Assert.True(viewModel.CanPublishTwoDImageHandoff); var commandTask = Task.Run(() => viewModel.PublishTwoDImageHandoffCommand.Execute(null)); @@ -414,14 +414,14 @@ public async Task TcpCommandsPushAndPullLatestTransactionWithoutRunningInspectio requestedConsumer), (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = consumer.ApplicationVersion; - viewModel.TwoDConsumerCommit = consumer.SourceCommit; - viewModel.TcpListenPortText = "45113"; - viewModel.TcpPeerPortText = endpoint.Port.ToString(); + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = consumer.ApplicationVersion; + viewModel.Setup.TwoDConsumerCommit = consumer.SourceCommit; + viewModel.Setup.TcpListenPortText = "45113"; + viewModel.Setup.TcpPeerPortText = endpoint.Port.ToString(); viewModel.SetSessionSharedKey(encodedKey); - viewModel.SaveSetupCommand.Execute(null); + viewModel.Setup.SaveSetupCommand.Execute(null); viewModel.PublishTwoDImageHandoffCommand.Execute(null); await WaitForAsync(() => !viewModel.IsBusy && viewModel.CanPushLatestTransaction); @@ -466,10 +466,10 @@ public async Task PublishAndRefreshUseExplicitCommandsAndProjectResult() requestedConsumer), (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = consumer.ApplicationVersion; - viewModel.TwoDConsumerCommit = consumer.SourceCommit; + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = consumer.ApplicationVersion; + viewModel.Setup.TwoDConsumerCommit = consumer.SourceCommit; Assert.True(viewModel.CanPublishTwoDImageHandoff); viewModel.PublishTwoDImageHandoffCommand.Execute(null); @@ -520,10 +520,10 @@ public async Task RefreshProjectsRejectedAcknowledgementWithoutPublishingResult( requestedConsumer), (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = consumer.ApplicationVersion; - viewModel.TwoDConsumerCommit = consumer.SourceCommit; + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = consumer.ApplicationVersion; + viewModel.Setup.TwoDConsumerCommit = consumer.SourceCommit; viewModel.PublishTwoDImageHandoffCommand.Execute(null); await WaitForAsync(() => !viewModel.IsBusy @@ -585,10 +585,10 @@ public async Task ResultFileChangeRefreshesProjectionStatusWithoutExplicitComman requestedConsumer), (_, _) => true); - viewModel.ExchangeRoot = fixture.ExchangeRoot; - viewModel.InspectionRecipePath = fixture.RecipePath; - viewModel.TwoDConsumerVersion = consumer.ApplicationVersion; - viewModel.TwoDConsumerCommit = consumer.SourceCommit; + viewModel.Setup.ExchangeRoot = fixture.ExchangeRoot; + viewModel.Setup.InspectionRecipePath = fixture.RecipePath; + viewModel.Setup.TwoDConsumerVersion = consumer.ApplicationVersion; + viewModel.Setup.TwoDConsumerCommit = consumer.SourceCommit; viewModel.PublishTwoDImageHandoffCommand.Execute(null); await WaitForAsync(() => !viewModel.IsBusy diff --git a/tests/OpenVisionLab.MachineStudio.Tests/OpenVisionLab.MachineStudio.Tests.csproj b/tests/OpenVisionLab.MachineStudio.Tests/OpenVisionLab.MachineStudio.Tests.csproj index 508da75..6f7f83e 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/OpenVisionLab.MachineStudio.Tests.csproj +++ b/tests/OpenVisionLab.MachineStudio.Tests/OpenVisionLab.MachineStudio.Tests.csproj @@ -23,6 +23,8 @@ + + diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectLifecycleCoordinatorTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectLifecycleCoordinatorTests.cs index 040a32c..fe32cba 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectLifecycleCoordinatorTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectLifecycleCoordinatorTests.cs @@ -1,4 +1,5 @@ using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.Model; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; @@ -44,7 +45,7 @@ public async Task SaveAndOpenKeepSessionStateInsideCoordinator() var openedProject = new MachineProjectDocument { Name = "Opened" }; var openPath = Path.Combine(directory, "opened.ovmachine"); - await new ProjectDocumentStore().SaveAsync(openedProject, openPath); + await new ProjectDocumentFileStore().SaveAsync(openedProject, openPath); Assert.True(await coordinator.OpenProjectAsync(openPath)); Assert.Equal(openedProject.Name, appliedProjects[0].Name); @@ -128,7 +129,7 @@ public async Task ReplacementOpenCancelLeavesTheCurrentSessionUntouched() var replacement = new MachineProjectDocument { Name = "Replacement" }; var replacementPath = Path.Combine(directory, "replacement.ovmachine"); - await new ProjectDocumentStore().SaveAsync(replacement, replacementPath); + await new ProjectDocumentFileStore().SaveAsync(replacement, replacementPath); Assert.False(await coordinator.OpenProjectReplacingCurrentAsync(replacementPath)); Assert.Equal("Edited", coordinator.CurrentProject.Name); diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectOpenWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectOpenWorkflowTests.cs index 3d36ce0..a0b0654 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectOpenWorkflowTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectOpenWorkflowTests.cs @@ -1,4 +1,5 @@ using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; @@ -13,7 +14,7 @@ public async Task OpenLoadsProjectAndPassesItToApplicationCallback() try { var path = Path.Combine(directory, "open.ovmachine"); - await new ProjectDocumentStore().SaveAsync( + await new ProjectDocumentFileStore().SaveAsync( new MachineProjectDocument { Name = "Loaded project" }, path); MachineProjectDocument? appliedProject = null; @@ -77,7 +78,7 @@ public async Task ReplacementChecksUnsavedChangesBeforeSecondLoadOrApply() try { var path = Path.Combine(directory, "replace.ovmachine"); - await new ProjectDocumentStore().SaveAsync( + await new ProjectDocumentFileStore().SaveAsync( new MachineProjectDocument { Name = "Replacement" }, path); var resolveCount = 0; @@ -110,7 +111,7 @@ private static ProjectOpenWorkflow CreateWorkflow( Func> applyOpenedProject, Action handleLoadFailure) => new( - new ProjectDocumentStore(), + new ProjectDocumentFileStore(), resolveUnsavedChanges, applyOpenedProject, handleLoadFailure); diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectSaveWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectSaveWorkflowTests.cs index ffeb61a..4e153b2 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectSaveWorkflowTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Project/ProjectSaveWorkflowTests.cs @@ -1,4 +1,5 @@ using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; @@ -112,6 +113,7 @@ private static ProjectSaveWorkflow CreateWorkflow( Action persistMultiAxisResult, Action persistVisionEvidence) => new( + new ProjectDocumentFileStore(), new ProjectDocumentStore(), () => project, prepareProject, diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Recipes/RecipeConnectionWorkbenchViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Recipes/RecipeConnectionWorkbenchViewModelTests.cs index 4348e8b..2a8c5de 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Recipes/RecipeConnectionWorkbenchViewModelTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Recipes/RecipeConnectionWorkbenchViewModelTests.cs @@ -31,19 +31,48 @@ public void SemanticEquipmentSetupViewModelOwnsEditabilityAndPreviewCommands() () => clearCount++); viewModel.Load(project); - Assert.True(viewModel.PreviewInspectionHandoffSetupCommand.CanExecute(null)); + var semantic = viewModel; + Assert.True(semantic.InspectionHandoff.PreviewCommand.CanExecute(null)); - viewModel.PreviewInspectionHandoffSetupCommand.Execute(null); + semantic.InspectionHandoff.PreviewCommand.Execute(null); Assert.Equal(1, clearCount); - Assert.True(viewModel.IsInspectionHandoffSetupVisible); - Assert.True(viewModel.CancelInspectionHandoffSetupCommand.CanExecute(null)); + Assert.True(semantic.InspectionHandoff.IsVisible); + Assert.True(semantic.InspectionHandoff.CancelCommand.CanExecute(null)); - viewModel.IsEditable = false; + semantic.IsEditable = false; + + Assert.False(semantic.InspectionHandoff.PreviewCommand.CanExecute(null)); + Assert.False(semantic.InspectionHandoff.ApplyCommand.CanExecute(null)); + Assert.True(semantic.InspectionHandoff.CancelCommand.CanExecute(null)); + } - Assert.False(viewModel.PreviewInspectionHandoffSetupCommand.CanExecute(null)); - Assert.False(viewModel.ApplyInspectionHandoffSetupCommand.CanExecute(null)); - Assert.True(viewModel.CancelInspectionHandoffSetupCommand.CanExecute(null)); + [Fact] + public void SemanticSetupChildrenKeepPreviewStateMutuallyExclusive() + { + var project = new ProjectDocumentStore().Load(File.ReadAllText(Path.Combine( + AppContext.BaseDirectory, + "SemiconductorRecipes", + "04-WaferOcrInspection.ovmachine"))); + SemanticEquipmentSetupViewModel? semantic = null; + semantic = new SemanticEquipmentSetupViewModel( + _ => 0, + _ => 0, + _ => 0, + _ => 0, + _ => 0, + () => semantic!.ClearPreviewForCompetingSetup()); + + semantic.Load(project); + semantic.WaferHandler.PreviewCommand.Execute(null); + Assert.True(semantic.WaferHandler.IsVisible); + Assert.False(semantic.Prealigner.IsVisible); + Assert.False(semantic.InspectionHandoff.IsVisible); + + semantic.Prealigner.PreviewCommand.Execute(null); + Assert.False(semantic.WaferHandler.IsVisible); + Assert.True(semantic.Prealigner.IsVisible); + Assert.False(semantic.InspectionHandoff.IsVisible); } [Fact] @@ -261,85 +290,87 @@ public void RefreshLocalizationPreservesSemanticSetupDraft( () => dryRunCount++, () => playbackCount++); viewModel.Load(project); + var semantic = viewModel.SemanticSetups; ICommand PreviewCommand() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.PreviewWaferHandlerSetupCommand, - "prealigner" => viewModel.SemanticSetups.PreviewPrealignerSetupCommand, - "inspection-handoff" => viewModel.SemanticSetups.PreviewInspectionHandoffSetupCommand, - "inspection-sort" => viewModel.SemanticSetups.PreviewInspectionSortRouterSetupCommand, - _ => viewModel.SemanticSetups.PreviewOhtHandoffSetupCommand + "wafer-handler" => semantic.WaferHandler.PreviewCommand, + "prealigner" => semantic.Prealigner.PreviewCommand, + "inspection-handoff" => semantic.InspectionHandoff.PreviewCommand, + "inspection-sort" => semantic.InspectionSortRouter.PreviewCommand, + _ => semantic.OhtHandoff.PreviewCommand }; ICommand ApplyCommand() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.ApplyWaferHandlerSetupCommand, - "prealigner" => viewModel.SemanticSetups.ApplyPrealignerSetupCommand, - "inspection-handoff" => viewModel.SemanticSetups.ApplyInspectionHandoffSetupCommand, - "inspection-sort" => viewModel.SemanticSetups.ApplyInspectionSortRouterSetupCommand, - _ => viewModel.SemanticSetups.ApplyOhtHandoffSetupCommand + "wafer-handler" => semantic.WaferHandler.ApplyCommand, + "prealigner" => semantic.Prealigner.ApplyCommand, + "inspection-handoff" => semantic.InspectionHandoff.ApplyCommand, + "inspection-sort" => semantic.InspectionSortRouter.ApplyCommand, + _ => semantic.OhtHandoff.ApplyCommand }; ICommand ResetCommand() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.ResetWaferHandlerSetupCommand, - "prealigner" => viewModel.SemanticSetups.ResetPrealignerSetupCommand, - "inspection-handoff" => viewModel.SemanticSetups.ResetInspectionHandoffSetupCommand, - "inspection-sort" => viewModel.SemanticSetups.ResetInspectionSortRouterSetupCommand, - _ => viewModel.SemanticSetups.ResetOhtHandoffSetupCommand + "wafer-handler" => semantic.WaferHandler.ResetCommand, + "prealigner" => semantic.Prealigner.ResetCommand, + "inspection-handoff" => semantic.InspectionHandoff.ResetCommand, + "inspection-sort" => semantic.InspectionSortRouter.ResetCommand, + _ => semantic.OhtHandoff.ResetCommand }; ICommand CancelCommand() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.CancelWaferHandlerSetupCommand, - "prealigner" => viewModel.SemanticSetups.CancelPrealignerSetupCommand, - "inspection-handoff" => viewModel.SemanticSetups.CancelInspectionHandoffSetupCommand, - "inspection-sort" => viewModel.SemanticSetups.CancelInspectionSortRouterSetupCommand, - _ => viewModel.SemanticSetups.CancelOhtHandoffSetupCommand + "wafer-handler" => semantic.WaferHandler.CancelCommand, + "prealigner" => semantic.Prealigner.CancelCommand, + "inspection-handoff" => semantic.InspectionHandoff.CancelCommand, + "inspection-sort" => semantic.InspectionSortRouter.CancelCommand, + _ => semantic.OhtHandoff.CancelCommand }; string? DraftValue() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.WaferHandlerHorizontalAxisId, - "prealigner" => viewModel.SemanticSetups.PrealignerRotaryStageComponentId, - "inspection-handoff" => viewModel.SemanticSetups.InspectionHandoffCameraId, - "inspection-sort" => viewModel.SemanticSetups.InspectionSortCameraId, - _ => viewModel.SemanticSetups.OhtTransportConveyorId + "wafer-handler" => semantic.WaferHandler.HorizontalAxisId, + "prealigner" => semantic.Prealigner.RotaryStageComponentId, + "inspection-handoff" => semantic.InspectionHandoff.CameraId, + "inspection-sort" => semantic.InspectionSortRouter.CameraId, + _ => semantic.OhtHandoff.TransportConveyorId }; void SetDraftValue(string value) { - if (setupKind == "wafer-handler") viewModel.SemanticSetups.WaferHandlerHorizontalAxisId = value; - else if (setupKind == "prealigner") viewModel.SemanticSetups.PrealignerRotaryStageComponentId = value; - else if (setupKind == "inspection-handoff") viewModel.SemanticSetups.InspectionHandoffCameraId = value; - else if (setupKind == "inspection-sort") viewModel.SemanticSetups.InspectionSortCameraId = value; - else viewModel.SemanticSetups.OhtTransportConveyorId = value; + if (setupKind == "wafer-handler") semantic.WaferHandler.HorizontalAxisId = value; + else if (setupKind == "prealigner") semantic.Prealigner.RotaryStageComponentId = value; + else if (setupKind == "inspection-handoff") semantic.InspectionHandoff.CameraId = value; + else if (setupKind == "inspection-sort") semantic.InspectionSortRouter.CameraId = value; + else semantic.OhtHandoff.TransportConveyorId = value; } ObservableCollection DraftOptions() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.WaferHandlerAxisOptions, - "prealigner" => viewModel.SemanticSetups.PrealignerStageOptions, - "oht" => viewModel.SemanticSetups.InspectionConveyorOptions, - _ => viewModel.SemanticSetups.InspectionCameraOptions + "wafer-handler" => semantic.WaferHandler.AxisOptions, + "prealigner" => semantic.Prealigner.StageOptions, + "oht" => semantic.OhtHandoff.ConveyorOptions, + "inspection-sort" => semantic.InspectionSortRouter.CameraOptions, + _ => semantic.InspectionHandoff.CameraOptions }; bool IsVisible() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.IsWaferHandlerSetupVisible, - "prealigner" => viewModel.SemanticSetups.IsPrealignerSetupVisible, - "inspection-handoff" => viewModel.SemanticSetups.IsInspectionHandoffSetupVisible, - "inspection-sort" => viewModel.SemanticSetups.IsInspectionSortRouterSetupVisible, - _ => viewModel.SemanticSetups.IsOhtHandoffSetupVisible + "wafer-handler" => semantic.WaferHandler.IsVisible, + "prealigner" => semantic.Prealigner.IsVisible, + "inspection-handoff" => semantic.InspectionHandoff.IsVisible, + "inspection-sort" => semantic.InspectionSortRouter.IsVisible, + _ => semantic.OhtHandoff.IsVisible }; bool HasValidationError() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.HasWaferHandlerSetupValidationError, - "prealigner" => viewModel.SemanticSetups.HasPrealignerSetupValidationError, - "inspection-handoff" => viewModel.SemanticSetups.HasInspectionHandoffSetupValidationError, - "inspection-sort" => viewModel.SemanticSetups.HasInspectionSortRouterSetupValidationError, - _ => viewModel.SemanticSetups.HasOhtHandoffSetupValidationError + "wafer-handler" => semantic.WaferHandler.HasValidationError, + "prealigner" => semantic.Prealigner.HasValidationError, + "inspection-handoff" => semantic.InspectionHandoff.HasValidationError, + "inspection-sort" => semantic.InspectionSortRouter.HasValidationError, + _ => semantic.OhtHandoff.HasValidationError }; string ValidationText() => setupKind switch { - "wafer-handler" => viewModel.SemanticSetups.WaferHandlerSetupValidationText, - "prealigner" => viewModel.SemanticSetups.PrealignerSetupValidationText, - "inspection-handoff" => viewModel.SemanticSetups.InspectionHandoffSetupValidationText, - "inspection-sort" => viewModel.SemanticSetups.InspectionSortRouterSetupValidationText, - _ => viewModel.SemanticSetups.OhtHandoffSetupValidationText + "wafer-handler" => semantic.WaferHandler.ValidationText, + "prealigner" => semantic.Prealigner.ValidationText, + "inspection-handoff" => semantic.InspectionHandoff.ValidationText, + "inspection-sort" => semantic.InspectionSortRouter.ValidationText, + _ => semantic.OhtHandoff.ValidationText }; PreviewCommand().Execute(null); diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Recipes/SemiconductorRecipeCopyWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Recipes/SemiconductorRecipeCopyWorkflowTests.cs index 45489da..58598df 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Recipes/SemiconductorRecipeCopyWorkflowTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Recipes/SemiconductorRecipeCopyWorkflowTests.cs @@ -1,4 +1,5 @@ using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Persistence.Projects; using OpenVisionLab.MachineStudio.ViewModel; using Xunit; @@ -22,11 +23,11 @@ public async Task CopyLoadsSourceCreatesNewIdentityAndSavesDestination() CreatedAt = DateTimeOffset.UtcNow.AddDays(-2), ModifiedAt = DateTimeOffset.UtcNow.AddDays(-1) }; - await new ProjectDocumentStore().SaveAsync(source, sourcePath); + await new ProjectDocumentFileStore().SaveAsync(source, sourcePath); var sourceBeforeCopy = new ProjectDocumentStore().Load( await File.ReadAllTextAsync(sourcePath)); var workflow = new SemiconductorRecipeCopyWorkflow( - new ProjectDocumentStore(), + new ProjectDocumentFileStore(), () => "same path"); var result = await workflow.CopyAsync(sourcePath, destinationPath); @@ -60,7 +61,7 @@ public async Task CopyRejectsSameSourceAndDestinationBeforeLoading() var path = Path.Combine(directory, "recipe.ovmachine"); var message = "cannot overwrite recipe"; var workflow = new SemiconductorRecipeCopyWorkflow( - new ProjectDocumentStore(), + new ProjectDocumentFileStore(), () => message); var exception = await Assert.ThrowsAsync(() => diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeObservabilityRetentionTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Evidence/RuntimeObservabilityRetentionTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeObservabilityRetentionTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Evidence/RuntimeObservabilityRetentionTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/FaultManagerViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Faults/FaultManagerViewModelTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/FaultManagerViewModelTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Faults/FaultManagerViewModelTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/ScheduledFaultScenarioWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Faults/ScheduledFaultScenarioWorkflowTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/ScheduledFaultScenarioWorkflowTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Faults/ScheduledFaultScenarioWorkflowTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeAlarmCollectionViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeAlarmCollectionViewModelTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeAlarmCollectionViewModelTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeAlarmCollectionViewModelTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeDebuggerViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeDebuggerViewModelTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeDebuggerViewModelTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeDebuggerViewModelTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeDebuggerWatchTargetCatalogTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeDebuggerWatchTargetCatalogTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeDebuggerWatchTargetCatalogTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeDebuggerWatchTargetCatalogTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeDefinitionApplicationWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeDefinitionApplicationWorkflowTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeDefinitionApplicationWorkflowTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeDefinitionApplicationWorkflowTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeObservabilityJournalTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeObservabilityJournalTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeObservabilityJournalTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeObservabilityJournalTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeObservabilityPresenterTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeObservabilityPresenterTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeObservabilityPresenterTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeObservabilityPresenterTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeShutdownTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeShutdownTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/RuntimeShutdownTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/RuntimeShutdownTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationCommandPresentationDispatcherTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationCommandPresentationDispatcherTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationCommandPresentationDispatcherTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationCommandPresentationDispatcherTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRunControlAdmissionPolicyTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRunControlAdmissionPolicyTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRunControlAdmissionPolicyTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRunControlAdmissionPolicyTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRunControlWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRunControlWorkflowTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRunControlWorkflowTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRunControlWorkflowTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeLoopTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeLoopTests.cs similarity index 72% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeLoopTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeLoopTests.cs index e9c7253..cc842a7 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeLoopTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeLoopTests.cs @@ -62,6 +62,64 @@ public async Task StartsOnceConfiguresAndDeliversSnapshotsBeforeCancellation() Assert.True(loop.IsCompleted); } + [Fact] + public async Task DispatchesEngineTerminationThroughThePresentationBoundary() + { + using var engine = new FixedStepSimulationEngine( + new SimulationSettings + { + FixedStep = TimeSpan.FromMilliseconds(1), + TimeScale = 1 + }); + var dispatchScope = false; + var terminationWasDispatched = false; + var initialRuntimeApplied = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + var terminationObserved = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + using var loop = new SimulationRuntimeLoop( + engine, + action => + { + var previousScope = dispatchScope; + dispatchScope = true; + try + { + action(); + } + finally + { + dispatchScope = previousScope; + } + + return Task.CompletedTask; + }, + _ => { }, + _ => { }, + () => initialRuntimeApplied.TrySetResult(true), + _ => { }, + _ => { }, + termination => + { + terminationWasDispatched = dispatchScope; + terminationObserved.TrySetResult(termination); + }, + _ => { }); + + loop.Start(new SimulationRuntimeConfiguration([], [], [])); + await initialRuntimeApplied.Task.WaitAsync(TimeSpan.FromSeconds(2)); + + await engine.StopAsync(); + loop.Cancel(); + await loop.RuntimeTask.WaitAsync(TimeSpan.FromSeconds(2)); + await loop.TerminationObservationTask.WaitAsync(TimeSpan.FromSeconds(2)); + + Assert.Equal( + SimulationEngineTerminationOutcome.Stopped, + (await terminationObserved.Task).Outcome); + Assert.True(terminationWasDispatched); + } + [Fact] public async Task ConsumesCanonicalJournalSeparatelyFromPresentationWindow() { diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeResourceOwnerTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeResourceOwnerTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeResourceOwnerTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeResourceOwnerTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeShutdownWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeShutdownWorkflowTests.cs similarity index 85% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeShutdownWorkflowTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeShutdownWorkflowTests.cs index 0c9c227..4a6f792 100644 --- a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeShutdownWorkflowTests.cs +++ b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeShutdownWorkflowTests.cs @@ -42,7 +42,12 @@ public async Task StopsAndDisposesRuntimeWithoutConstructingMainViewModel() loop, resources, runControl, - diagnostics.Add); + diagnostics.Add, + static action => + { + action(); + return Task.CompletedTask; + }); loop.Start(new SimulationRuntimeConfiguration([], [], [])); var result = await workflow.ShutdownAsync(TimeSpan.FromSeconds(2)); @@ -88,25 +93,43 @@ public async Task CompletesApplicationDisposeAfterRuntimeResourcesAreDisposed() var workspace = new SimulationWorkspaceViewModel(); var resources = new SimulationRuntimeResourceOwner(engine, loop, workspace); var runControl = CreateRunControlWorkflow(engine); + var dispatchCount = 0; var workflow = new SimulationRuntimeShutdownWorkflow( engine, loop, resources, runControl, - _ => { }); + _ => { }, + action => + { + Interlocked.Increment(ref dispatchCount); + action(); + return Task.CompletedTask; + }); var callback = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); + var releaseDisposeDispatch = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); loop.Start(new SimulationRuntimeConfiguration([], [], [])); var shutdownTask = workflow.ShutdownAsync(TimeSpan.FromSeconds(2)); + var shutdownResult = await shutdownTask; + var delayedShutdownTask = Task.Run(async () => + { + await shutdownTask; + await releaseDisposeDispatch.Task; + return shutdownResult; + }); workflow.CompleteDisposeAfterShutdown( - shutdownTask, + delayedShutdownTask, () => callback.SetResult(resources.IsDisposed)); - var result = await shutdownTask; - Assert.Equal(RuntimeShutdownOutcome.Completed, result.Outcome); + releaseDisposeDispatch.TrySetResult(true); + + Assert.Equal(RuntimeShutdownOutcome.Completed, shutdownResult.Outcome); Assert.True(await callback.Task); Assert.True(resources.IsDisposed); + Assert.Equal(1, Volatile.Read(ref dispatchCount)); } [Fact] @@ -144,7 +167,12 @@ public async Task MarksShutdownIncompleteWhenCanonicalJournalBudgetIsExceeded() loop, resources, runControl, - diagnostics.Add); + diagnostics.Add, + static action => + { + action(); + return Task.CompletedTask; + }); loop.Start(new SimulationRuntimeConfiguration([], [], [])); await WaitForAsync(() => engine.EventJournal.TotalEventCount >= 2); diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeSnapshotProjectionTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeSnapshotProjectionTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationRuntimeSnapshotProjectionTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationRuntimeSnapshotProjectionTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationSessionCloseWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationSessionCloseWorkflowTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationSessionCloseWorkflowTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationSessionCloseWorkflowTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationSessionCoordinatorTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationSessionCoordinatorTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationSessionCoordinatorTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Runtime/SimulationSessionCoordinatorTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchArtifactStoreTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchArtifactStoreTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchArtifactStoreTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchArtifactStoreTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchParticipantTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchParticipantTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchParticipantTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchParticipantTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchPresentationTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchPresentationTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchPresentationTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchPresentationTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchRepetitionRunnerTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchRepetitionRunnerTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchRepetitionRunnerTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchRepetitionRunnerTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchViewModelTests.cs new file mode 100644 index 0000000..71563bf --- /dev/null +++ b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioBatchViewModelTests.cs @@ -0,0 +1,248 @@ +using OpenVisionLab; +using OpenVisionLab.Machine.Core.Channels; +using OpenVisionLab.Machine.Core.Projects; +using OpenVisionLab.Machine.Sequence.Compilation; +using OpenVisionLab.Machine.Simulation.Axis; +using OpenVisionLab.Machine.Simulation.Engine; +using OpenVisionLab.MachineStudio.ViewModel; +using OpenVisionLab.MachineStudio.ViewModel.Simulation; +using Xunit; + +namespace OpenVisionLab.MachineStudio.Tests; + +public sealed class SimulationScenarioBatchViewModelTests +{ + [Fact] + public void ParentGateAndResetAreOwnedByScenarioBatchViewModel() + { + OpenVisionLanguageService.Load(); + using var workspace = new SimulationWorkspaceViewModel(); + var project = new MachineProjectDocument { Name = "Batch owner test" }; + var parentAllowsRun = true; + var otherValidationRunning = false; + var parentNotifications = 0; + using var viewModel = CreateViewModel( + workspace, + project, + () => parentAllowsRun, + () => otherValidationRunning, + _ => parentNotifications++); + + Assert.True(viewModel.CanRunScenarioBatch); + Assert.True(viewModel.RunCommand.CanExecute(null)); + Assert.True(viewModel.IsScenarioConfigurationEnabled); + + parentAllowsRun = false; + viewModel.InvalidateCommands(); + Assert.False(viewModel.CanRunScenarioBatch); + Assert.False(viewModel.RunCommand.CanExecute(null)); + + otherValidationRunning = true; + Assert.False(viewModel.IsScenarioConfigurationEnabled); + Assert.False(viewModel.CanImportEvidence); + viewModel.Reset(); + + Assert.False(viewModel.IsBatchRunning); + Assert.Equal(0, viewModel.BatchCompletedRuns); + Assert.Null(viewModel.LatestBatchResult); + Assert.Null(viewModel.AcceptedBatchBaseline); + Assert.True(parentNotifications > 0); + } + + [Fact] + public async Task BatchCompletionUsesTheUiDispatchBoundary() + { + OpenVisionLanguageService.Load(); + using var workspace = new SimulationWorkspaceViewModel + { + ScenarioTargetId = "axis-1", + ScenarioDurationCycles = 3, + BatchRepetitionCount = 2 + }; + var project = new MachineProjectDocument { Name = "Batch dispatch test" }; + var dispatchScope = false; + var outsideDispatch = new List(); + var propertyChangedOutsideDispatch = false; + var parentNotificationsOutsideDispatch = false; + using var viewModel = CreateViewModel( + workspace, + project, + () => true, + () => false, + _ => + { + if (!dispatchScope) + { + parentNotificationsOutsideDispatch = true; + } + }, + action => + { + var previousScope = dispatchScope; + dispatchScope = true; + try + { + action(); + } + finally + { + dispatchScope = previousScope; + } + + return Task.CompletedTask; + }, + status => + { + if (!dispatchScope) + { + outsideDispatch.Add($"status:{status}"); + } + }, + log => + { + if (!dispatchScope) + { + outsideDispatch.Add($"log:{log}"); + } + }, + buildRuntime: CreateRuntime); + viewModel.PropertyChanged += (_, _) => + { + if (!dispatchScope) + { + propertyChangedOutsideDispatch = true; + } + }; + + viewModel.RunCommand.Execute(null); + var batchTask = viewModel.BatchTask; + Assert.NotNull(batchTask); + await batchTask!.WaitAsync(TimeSpan.FromSeconds(10)); + + Assert.True(viewModel.LatestBatchResult?.IsComplete); + Assert.True(viewModel.LatestBatchResult?.IsSuccess); + Assert.False(viewModel.IsBatchRunning); + Assert.Empty(outsideDispatch); + Assert.False(propertyChangedOutsideDispatch); + Assert.False(parentNotificationsOutsideDispatch); + } + + [Fact] + public async Task BatchCancellationUsesTheUiDispatchBoundary() + { + OpenVisionLanguageService.Load(); + using var workspace = new SimulationWorkspaceViewModel + { + ScenarioTargetId = "axis-1", + ScenarioDurationCycles = 100_000, + BatchRepetitionCount = 1 + }; + var project = new MachineProjectDocument { Name = "Batch cancellation dispatch test" }; + var dispatchScope = false; + var outsideDispatch = new List(); + var propertyChangedOutsideDispatch = false; + using var viewModel = CreateViewModel( + workspace, + project, + () => true, + () => false, + _ => { }, + action => + { + var previousScope = dispatchScope; + dispatchScope = true; + try + { + action(); + } + finally + { + dispatchScope = previousScope; + } + + return Task.CompletedTask; + }, + status => + { + if (!dispatchScope) + { + outsideDispatch.Add($"status:{status}"); + } + }, + log => + { + if (!dispatchScope) + { + outsideDispatch.Add($"log:{log}"); + } + }, + buildRuntime: CreateRuntime); + viewModel.PropertyChanged += (_, _) => + { + if (!dispatchScope) + { + propertyChangedOutsideDispatch = true; + } + }; + + viewModel.RunCommand.Execute(null); + var batchTask = viewModel.BatchTask; + Assert.NotNull(batchTask); + for (var attempt = 0; attempt < 100 && !viewModel.IsBatchRunning; attempt++) + { + await Task.Delay(10); + } + + Assert.True(viewModel.IsBatchRunning); + viewModel.CancelCommand.Execute(null); + await batchTask!.WaitAsync(TimeSpan.FromSeconds(10)); + + Assert.True(viewModel.BatchWasCanceled); + Assert.False(viewModel.IsBatchRunning); + Assert.Empty(outsideDispatch); + Assert.False(propertyChangedOutsideDispatch); + } + + private static SimulationScenarioBatchViewModel CreateViewModel( + SimulationWorkspaceViewModel workspace, + MachineProjectDocument project, + Func parentGate, + Func otherValidationGate, + Action notifyParent, + Func? dispatchToUi = null, + Action? setStatus = null, + Action? log = null, + Func? buildRuntime = null) => + new( + workspace, + parentGate, + parentGate, + parentGate, + otherValidationGate, + () => project, + () => null, + () => Task.FromResult(true), + () => false, + () => Task.FromResult(true), + _ => { }, + buildRuntime ?? (() => throw new InvalidOperationException("The batch runner is not part of this test.")), + () => "{}", + TimeSpan.FromMilliseconds(5), + dispatchToUi ?? (action => + { + action(); + return Task.CompletedTask; + }), + () => Array.Empty(), + () => { }, + setStatus ?? (_ => { }), + log ?? (_ => { }), + _ => { }, + notifyParent, + _ => { }); + + private static SimulationRuntimeConfiguration CreateRuntime() => new( + [new AxisConfiguration { Id = "axis-1", Name = "Test axis" }], + Array.Empty(), + Array.Empty()); +} diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioExecutionCoordinatorTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioExecutionCoordinatorTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioExecutionCoordinatorTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioExecutionCoordinatorTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioProfileTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioProfileTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioProfileTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioProfileTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioProjectMapperTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioProjectMapperTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioProjectMapperTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioProjectMapperTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioWorkflowTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioWorkflowTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioWorkflowTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Scenarios/SimulationScenarioWorkflowTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchViewModelTests.cs deleted file mode 100644 index 1e35161..0000000 --- a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationScenarioBatchViewModelTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using OpenVisionLab; -using OpenVisionLab.Machine.Core.Projects; -using OpenVisionLab.Machine.Simulation.Engine; -using OpenVisionLab.MachineStudio.ViewModel; -using OpenVisionLab.MachineStudio.ViewModel.Simulation; -using Xunit; - -namespace OpenVisionLab.MachineStudio.Tests; - -public sealed class SimulationScenarioBatchViewModelTests -{ - [Fact] - public void ParentGateAndResetAreOwnedByScenarioBatchViewModel() - { - OpenVisionLanguageService.Load(); - using var workspace = new SimulationWorkspaceViewModel(); - var project = new MachineProjectDocument { Name = "Batch owner test" }; - var parentAllowsRun = true; - var otherValidationRunning = false; - var parentNotifications = 0; - using var viewModel = CreateViewModel( - workspace, - project, - () => parentAllowsRun, - () => otherValidationRunning, - _ => parentNotifications++); - - Assert.True(viewModel.CanRunScenarioBatch); - Assert.True(viewModel.RunCommand.CanExecute(null)); - Assert.True(viewModel.IsScenarioConfigurationEnabled); - - parentAllowsRun = false; - viewModel.InvalidateCommands(); - Assert.False(viewModel.CanRunScenarioBatch); - Assert.False(viewModel.RunCommand.CanExecute(null)); - - otherValidationRunning = true; - Assert.False(viewModel.IsScenarioConfigurationEnabled); - Assert.False(viewModel.CanImportEvidence); - viewModel.Reset(); - - Assert.False(viewModel.IsBatchRunning); - Assert.Equal(0, viewModel.BatchCompletedRuns); - Assert.Null(viewModel.LatestBatchResult); - Assert.Null(viewModel.AcceptedBatchBaseline); - Assert.True(parentNotifications > 0); - } - - private static SimulationScenarioBatchViewModel CreateViewModel( - SimulationWorkspaceViewModel workspace, - MachineProjectDocument project, - Func parentGate, - Func otherValidationGate, - Action notifyParent) => - new( - workspace, - parentGate, - parentGate, - parentGate, - otherValidationGate, - () => project, - () => null, - () => Task.FromResult(true), - () => false, - () => Task.FromResult(true), - _ => { }, - () => throw new InvalidOperationException("The batch runner is not part of this test."), - () => "{}", - TimeSpan.FromMilliseconds(5), - action => - { - action(); - return Task.CompletedTask; - }, - () => Array.Empty(), - () => { }, - _ => { }, - _ => { }, - _ => { }, - notifyParent, - _ => { }); -} diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/CommandTraceWorkflowViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Trace/CommandTraceWorkflowViewModelTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/CommandTraceWorkflowViewModelTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Trace/CommandTraceWorkflowViewModelTests.cs diff --git a/tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationCommandTraceViewModelTests.cs b/tests/OpenVisionLab.MachineStudio.Tests/Simulation/Trace/SimulationCommandTraceViewModelTests.cs similarity index 100% rename from tests/OpenVisionLab.MachineStudio.Tests/Simulation/SimulationCommandTraceViewModelTests.cs rename to tests/OpenVisionLab.MachineStudio.Tests/Simulation/Trace/SimulationCommandTraceViewModelTests.cs