473 lines
21 KiB
C#
473 lines
21 KiB
C#
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));
|
||
}
|
||
}
|
||
}
|
||
}
|