Files
XplorePlane/XplorePlane/Services/Cnc/CncExecutionService.cs
T
2026-05-06 23:18:28 +08:00

473 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using Prism.Events;
using XP.Common.Converters;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport;
using XplorePlane.ViewModels;
namespace XplorePlane.Services.Cnc
{
/// <summary>
/// Executes a CNC program node-by-node, reporting progress and persisting inspection results.
/// </summary>
public class CncExecutionService : ICncExecutionService
{
private readonly IInspectionResultStore _store;
private readonly ILoggerService _logger;
private readonly IMainViewportService _mainViewportService;
private readonly IAppStateService _appStateService;
private readonly IPipelineExecutionService _pipelineExecutionService;
private readonly IImageProcessingService _imageProcessingService;
private readonly IEventAggregator _eventAggregator;
// Task 4.2: volatile field so reads/writes are not reordered across threads
private volatile CancellationTokenSource _executionCts;
public CncExecutionService(
IInspectionResultStore store,
ILoggerService logger,
IMainViewportService mainViewportService,
IAppStateService appStateService,
IPipelineExecutionService pipelineExecutionService,
IImageProcessingService imageProcessingService,
IEventAggregator eventAggregator)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mainViewportService = mainViewportService;
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_pipelineExecutionService = pipelineExecutionService;
_imageProcessingService = imageProcessingService;
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
// Task 4.3: subscribe to DetectorDisconnectedEvent on a background thread
_eventAggregator.GetEvent<DetectorDisconnectedEvent>()
.Subscribe(OnDetectorDisconnected, ThreadOption.BackgroundThread);
}
// Task 4.3: callback cancel the running execution when the detector disconnects
private void OnDetectorDisconnected()
{
var cts = _executionCts;
if (cts == null) return;
try
{
cts.Cancel();
_logger.ForModule<CncExecutionService>().Warn("探测器断连,已取消当前 CNC 执行");
}
catch (ObjectDisposedException) { }
}
public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken)
{
// Task 4.4: create a linked CTS so DetectorDisconnectedEvent can cancel independently
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executionCts = linkedCts;
_mainViewportService?.SetCncRunning(true);
try
{
// Pre-cancellation check - do NOT call BeginRunAsync if already cancelled
if (linkedCts.Token.IsCancellationRequested)
return;
int inspectionNodeCount = program.Nodes.OfType<InspectionModuleNode>().Count();
var sourceImage = TryGetSourceImage();
Guid runId;
try
{
var runRecord = new InspectionRunRecord
{
ProgramName = program.Name,
NodeCount = inspectionNodeCount,
StartedAt = DateTime.UtcNow
};
InspectionAssetWriteRequest sourceAsset = null;
if (sourceImage != null)
{
sourceAsset = new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.RunSourceImage,
Content = EncodeBitmapToBmp(sourceImage),
FileFormat = "bmp",
Width = sourceImage.PixelWidth,
Height = sourceImage.PixelHeight
};
}
await _store.BeginRunAsync(runRecord, sourceAsset);
runId = runRecord.RunId;
}
catch (Exception ex)
{
_logger.ForModule<CncExecutionService>().Error(ex, "Failed to begin inspection run for program '{0}'", program.Name);
return;
}
bool cancelled = false;
bool allSucceeded = true;
BitmapSource lastResultImage = null;
foreach (var node in program.Nodes.OrderBy(n => n.Index))
{
if (linkedCts.Token.IsCancellationRequested)
{
cancelled = true;
break;
}
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0));
bool nodeSucceeded = true;
try
{
switch (node)
{
case ReferencePointNode rp:
_logger.ForModule<CncExecutionService>().Info(
"Executing reference point node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation} " +
"RayOn={RayOn} Voltage={Voltage}kV Current={Current}uA",
rp.Index, rp.Name,
rp.StageX, rp.StageY, rp.SourceZ, rp.DetectorZ,
rp.DetectorSwing, rp.FDD, rp.FOD, rp.Magnification,
rp.StageRotation, rp.FixtureRotation,
rp.IsRayOn, rp.Voltage, rp.Current);
break;
case SavePositionNode sp:
_logger.ForModule<CncExecutionService>().Info(
"Executing save-position node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation}",
sp.Index, sp.Name,
sp.MotionState.StageX, sp.MotionState.StageY,
sp.MotionState.SourceZ, sp.MotionState.DetectorZ,
sp.MotionState.DetectorSwing, sp.MotionState.FDD,
sp.MotionState.FOD, sp.MotionState.Magnification,
sp.MotionState.StageRotation, sp.MotionState.FixtureRotation);
break;
case SaveNodeNode sn:
_logger.ForModule<CncExecutionService>().Info(
"Executing save node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W",
sn.Index, sn.Name,
sn.MotionState.StageX, sn.MotionState.StageY,
sn.MotionState.SourceZ, sn.MotionState.DetectorZ,
sn.MotionState.DetectorSwing, sn.MotionState.FDD,
sn.MotionState.FOD, sn.MotionState.Magnification,
sn.RaySourceState.IsOn, sn.RaySourceState.Voltage, sn.RaySourceState.Power);
break;
case SaveNodeWithImageNode sni:
_logger.ForModule<CncExecutionService>().Info(
"Executing save-with-image node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}",
sni.Index, sni.Name,
sni.MotionState.StageX, sni.MotionState.StageY,
sni.MotionState.SourceZ, sni.MotionState.DetectorZ,
sni.MotionState.DetectorSwing, sni.MotionState.FDD,
sni.MotionState.FOD, sni.MotionState.Magnification,
sni.RaySourceState.IsOn, sni.RaySourceState.Voltage, sni.RaySourceState.Power,
sni.ImageFileName);
break;
case WaitDelayNode waitNode:
try
{
await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token);
}
catch (OperationCanceledException)
{
cancelled = true;
}
break;
case PauseDialogNode pauseNode:
await Application.Current.Dispatcher.InvokeAsync(() =>
MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle));
if (linkedCts.Token.IsCancellationRequested)
cancelled = true;
break;
case InspectionModuleNode inspectionNode:
try
{
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, sourceImage, linkedCts.Token);
if (img != null) lastResultImage = img;
}
catch (Exception ex)
{
_logger.ForModule<CncExecutionService>().Error(ex,
"Failed to append node result for node '{0}' (Id={1})", inspectionNode.Name, inspectionNode.Id);
nodeSucceeded = false;
}
break;
case CompleteProgramNode:
// Report Succeeded before terminating the loop
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Succeeded));
goto endLoop;
default:
// Unknown node types are treated as succeeded
break;
}
}
catch (Exception ex)
{
_logger.ForModule<CncExecutionService>().Error(ex,
"Unexpected error executing node '{0}' (Id={1})", node.Name, node.Id);
if (linkedCts.Token.IsCancellationRequested)
cancelled = true;
else
nodeSucceeded = false;
}
if (cancelled)
{
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Failed));
break;
}
// Carry the latest inspection result image so the ViewModel can cache it on the node.
var nodeResultImage = node is InspectionModuleNode ? lastResultImage : null;
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage));
if (!nodeSucceeded)
allSucceeded = false;
}
endLoop:
bool? overallPass = cancelled ? null : (bool?)allSucceeded;
try
{
await _store.CompleteRunAsync(runId, overallPass);
}
catch (Exception ex)
{
_logger.ForModule<CncExecutionService>().Error(ex,
"Failed to complete inspection run '{0}'", runId);
}
} // end try
finally
{
_executionCts = null;
_mainViewportService?.SetCncRunning(false);
}
}
private BitmapSource TryGetSourceImage()
{
var viewportImage = _mainViewportService?.LatestManualImage as BitmapSource
?? _mainViewportService?.CurrentDisplayImage as BitmapSource;
if (viewportImage != null)
return viewportImage;
var detectorFrame = _appStateService?.LatestDetectorFrame;
if (detectorFrame?.ImageData == null || detectorFrame.Width <= 0 || detectorFrame.Height <= 0)
return null;
var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(
detectorFrame.ImageData,
(int)detectorFrame.Width,
(int)detectorFrame.Height);
bitmap.Freeze();
return bitmap;
}
private async Task<BitmapSource> ExecuteInspectionNodeAsync(
Guid runId,
InspectionModuleNode inspectionNode,
BitmapSource sourceImage,
CancellationToken cancellationToken)
{
var nodeResult = new InspectionNodeResult
{
RunId = runId,
NodeId = inspectionNode.Id,
NodeIndex = inspectionNode.Index,
NodeName = inspectionNode.Name,
PipelineName = inspectionNode.Pipeline?.Name ?? string.Empty
};
PipelineExecutionSnapshot pipelineSnapshot = null;
if (inspectionNode.Pipeline != null)
{
var pipelineJson = JsonSerializer.Serialize(inspectionNode.Pipeline);
pipelineSnapshot = new PipelineExecutionSnapshot
{
RunId = runId,
NodeId = inspectionNode.Id,
PipelineName = inspectionNode.Pipeline.Name,
PipelineDefinitionJson = pipelineJson
};
}
var assets = new System.Collections.Generic.List<InspectionAssetWriteRequest>();
if (sourceImage != null)
{
assets.Add(new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.NodeInputImage,
Content = EncodeBitmapToBmp(sourceImage),
FileFormat = "bmp",
Width = sourceImage.PixelWidth,
Height = sourceImage.PixelHeight
});
}
BitmapSource resultImage = null;
if (_pipelineExecutionService != null && inspectionNode.Pipeline?.Nodes?.Count > 0 && sourceImage != null)
{
try
{
var pipelineNodes = BuildPipelineNodeViewModels(inspectionNode.Pipeline);
resultImage = await _pipelineExecutionService.ExecutePipelineAsync(
pipelineNodes, sourceImage, null, cancellationToken);
if (resultImage != null)
{
assets.Add(new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.NodeResultImage,
Content = EncodeBitmapToBmp(resultImage),
FileFormat = "bmp",
Width = resultImage.PixelWidth,
Height = resultImage.PixelHeight
});
nodeResult.Status = InspectionNodeStatus.Succeeded;
_mainViewportService?.SetManualImage(resultImage, $"CNC Node: {inspectionNode.Name}");
}
}
catch (Exception ex)
{
_logger.ForModule<CncExecutionService>().Warn(
"Pipeline execution failed for node '{0}': {1}", inspectionNode.Name, ex.Message);
nodeResult.Status = InspectionNodeStatus.Failed;
}
}
await _store.AppendNodeResultAsync(nodeResult, pipelineSnapshot: pipelineSnapshot, assets: assets);
return resultImage;
}
private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
{
var nodes = new System.Collections.Generic.List<PipelineNodeViewModel>();
if (pipeline?.Nodes == null) return nodes;
foreach (var nodeModel in pipeline.Nodes.OrderBy(n => n.Order))
{
var displayName = _imageProcessingService?.GetProcessorDisplayName(nodeModel.OperatorKey) ?? nodeModel.OperatorKey;
var vm = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, string.Empty)
{
Order = nodeModel.Order,
IsEnabled = nodeModel.IsEnabled
};
if (_imageProcessingService != null)
{
var paramDefs = _imageProcessingService.GetProcessorParameters(nodeModel.OperatorKey);
if (paramDefs != null)
{
foreach (var def in paramDefs)
{
var paramVm = new ProcessorParameterVM(def);
if (nodeModel.Parameters != null && nodeModel.Parameters.TryGetValue(def.Name, out var saved))
paramVm.Value = ConvertSavedValue(saved, def.ValueType);
vm.Parameters.Add(paramVm);
}
}
}
nodes.Add(vm);
}
return nodes;
}
private static object ConvertSavedValue(object savedValue, Type targetType)
{
if (savedValue is not JsonElement jsonElement)
return savedValue;
try
{
if (targetType == typeof(int)) return jsonElement.GetInt32();
if (targetType == typeof(double)) return jsonElement.GetDouble();
if (targetType == typeof(bool)) return jsonElement.GetBoolean();
if (targetType == typeof(string)) return jsonElement.GetString() ?? string.Empty;
return jsonElement.ToString();
}
catch
{
return jsonElement.ToString();
}
}
private static byte[] EncodeBitmapToBmp(BitmapSource bitmap)
{
using var ms = new MemoryStream();
var encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(ms);
return ms.ToArray();
}
private static async Task ExecuteWaitDelayWithProgressAsync(
WaitDelayNode waitNode,
IProgress<CncNodeExecutionProgress> progress,
CancellationToken cancellationToken)
{
int totalMs = waitNode.DelayMilliseconds;
if (totalMs <= 0)
{
progress?.Report(new CncNodeExecutionProgress(waitNode.Id, NodeExecutionState.Running, ProgressPercent: 100));
return;
}
const int tickMs = 50;
int elapsed = 0;
while (elapsed < totalMs)
{
cancellationToken.ThrowIfCancellationRequested();
int remaining = totalMs - elapsed;
int delay = Math.Min(tickMs, remaining);
await Task.Delay(delay, cancellationToken);
elapsed += delay;
progress?.Report(new CncNodeExecutionProgress(
waitNode.Id,
NodeExecutionState.Running,
ProgressPercent: elapsed * 100d / totalMs));
}
}
}
}