diff --git a/DataBase/XP.db b/DataBase/XP.db index c79ddc0..f9d8e07 100644 Binary files a/DataBase/XP.db and b/DataBase/XP.db differ diff --git a/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs b/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs index 88b0704..ba7df01 100644 --- a/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs +++ b/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs @@ -19,6 +19,11 @@ namespace XP.Hardware.Detector.Abstractions.Enums /// /// Hamamatsu 探测器 | Hamamatsu detector (预留) /// - Hamamatsu = 2 + Hamamatsu = 2, + + /// + /// 软件模拟探测器,用于集成验证(无需硬件)| Software simulated detector for integration testing (no hardware required) + /// + Simulated = 99 } } diff --git a/XP.Hardware.Detector/Config/ConfigLoader.cs b/XP.Hardware.Detector/Config/ConfigLoader.cs index bc9d512..5225eaf 100644 --- a/XP.Hardware.Detector/Config/ConfigLoader.cs +++ b/XP.Hardware.Detector/Config/ConfigLoader.cs @@ -38,6 +38,7 @@ namespace XP.Hardware.Detector.Config { DetectorType.Varex => LoadVarexConfiguration(), DetectorType.IRay => LoadIRayConfiguration(), + DetectorType.Simulated => LoadSimulatedConfiguration(), _ => throw new NotSupportedException($"不支持的探测器类型:{detectorType} | Unsupported detector type: {detectorType}") }; @@ -111,6 +112,27 @@ namespace XP.Hardware.Detector.Config return config; } + /// + /// 加载软件模拟探测器配置 | Load simulated detector configuration + /// + private static SimulatedDetectorConfig LoadSimulatedConfiguration() + { + var config = new SimulatedDetectorConfig(); + + if (int.TryParse(ConfigurationManager.AppSettings["Detector:Simulated:Width"], out var w) && w > 0) + config.Width = w; + + if (int.TryParse(ConfigurationManager.AppSettings["Detector:Simulated:Height"], out var h) && h > 0) + config.Height = h; + + if (double.TryParse(ConfigurationManager.AppSettings["Detector:Simulated:FrameRateFps"], + System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out var fps) && fps > 0) + config.FrameRateFps = fps; + + return config; + } + /// /// 加载 iRay 配置 | Load iRay configuration /// @@ -208,16 +230,20 @@ namespace XP.Hardware.Detector.Config /// private static DetectorResult ValidateConfiguration(DetectorConfig config) { - // 验证 IP 地址 | Validate IP address - if (!IPAddress.TryParse(config.IP, out _)) + // 模拟探测器不需要网络连接,跳过 IP/端口验证 | Simulated detector skips IP/port validation + if (config.Type != DetectorType.Simulated) { - return DetectorResult.Failure($"无效的 IP 地址:{config.IP} | Invalid IP address: {config.IP}"); - } + // 验证 IP 地址 | Validate IP address + if (!IPAddress.TryParse(config.IP, out _)) + { + return DetectorResult.Failure($"无效的 IP 地址:{config.IP} | Invalid IP address: {config.IP}"); + } - // 验证端口范围 | Validate port range - if (config.Port < 1 || config.Port > 65535) - { - return DetectorResult.Failure($"无效的端口号:{config.Port},必须在 1-65535 之间 | Invalid port: {config.Port}, must be between 1-65535"); + // 验证端口范围 | Validate port range + if (config.Port < 1 || config.Port > 65535) + { + return DetectorResult.Failure($"无效的端口号:{config.Port},必须在 1-65535 之间 | Invalid port: {config.Port}, must be between 1-65535"); + } } // 验证存储路径 | Validate save path diff --git a/XP.Hardware.Detector/Factories/DetectorFactory.cs b/XP.Hardware.Detector/Factories/DetectorFactory.cs index 8711cde..7732c04 100644 --- a/XP.Hardware.Detector/Factories/DetectorFactory.cs +++ b/XP.Hardware.Detector/Factories/DetectorFactory.cs @@ -7,7 +7,6 @@ using XP.Hardware.Detector.Abstractions.Enums; using XP.Hardware.Detector.Config; using XP.Hardware.Detector.Implementations; using XP.Common.Logging.Interfaces; - namespace XP.Hardware.Detector.Factories { /// @@ -54,6 +53,7 @@ namespace XP.Hardware.Detector.Factories DetectorType.Varex => CreateVarexDetector(config), //DetectorType.IRay => CreateIRayDetector(config), DetectorType.Hamamatsu => throw new NotImplementedException("Hamamatsu 探测器尚未实现 | Hamamatsu detector not implemented yet"), + DetectorType.Simulated => CreateSimulatedDetector(config), _ => throw new NotSupportedException($"不支持的探测器类型:{config.Type} | Unsupported detector type: {config.Type}") }; @@ -113,7 +113,21 @@ namespace XP.Hardware.Detector.Factories /// public IEnumerable GetSupportedTypes() { - return new[] { DetectorType.Varex, DetectorType.IRay }; + return new[] { DetectorType.Varex, DetectorType.IRay, DetectorType.Simulated }; + } + + /// + /// 创建软件模拟探测器 | Create simulated detector + /// + private IAreaDetector CreateSimulatedDetector(DetectorConfig config) + { + var simConfig = config as SimulatedDetectorConfig + ?? new SimulatedDetectorConfig(); + + _logger?.Info("[DetectorFactory] 创建 SimulatedDetector,分辨率 {W}x{H},帧率 {FPS} fps", + simConfig.Width, simConfig.Height, simConfig.FrameRateFps); + + return new SimulatedDetector(simConfig, _eventAggregator, _logger); } } } diff --git a/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json b/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json index a3e12f2..6861e9d 100644 --- a/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json +++ b/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json @@ -1705,7 +1705,10 @@ "Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408" }, "runtime": { - "XP.Common.dll": {} + "XP.Common.dll": { + "assemblyVersion": "1.4.16.1", + "fileVersion": "1.4.16.1" + } }, "resources": { "en-US/XP.Common.resources.dll": { diff --git a/XplorePlane/App.config b/XplorePlane/App.config index 5fd6b21..79c6b82 100644 --- a/XplorePlane/App.config +++ b/XplorePlane/App.config @@ -57,13 +57,18 @@ - + + + + + + diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index 6298f43..1e4b341 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -27,6 +27,7 @@ using XP.Common.Logging.Implementations; using XP.Common.Logging.Interfaces; using XP.Common.Module; using XP.Hardware.Detector.Module; +using XP.Hardware.Detector.Services; using XP.Hardware.MotionControl.Module; using XP.Hardware.PLC; using XP.Hardware.RaySource.Module; @@ -298,7 +299,7 @@ namespace XplorePlane var shell = Container.Resolve(); // 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化 - shell.Loaded += (s, e) => + shell.Loaded += async (s, e) => { // [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度 // TryConnectCamera(); @@ -313,6 +314,9 @@ namespace XplorePlane Log.Error(ex, "初始化主界面探测器帧流水线失败"); } + // 若配置为模拟探测器,自动初始化并启动采集(无需用户手动操作) + await TryAutoStartSimulatedDetectorAsync(); + // [DEV] 相机状态通知已屏蔽 // try // { @@ -333,6 +337,44 @@ namespace XplorePlane return shell; } + /// + /// 若 App.config 中 Detector:Type = Simulated,自动初始化并启动连续采集。 + /// 真实硬件类型不受影响。 + /// + private async System.Threading.Tasks.Task TryAutoStartSimulatedDetectorAsync() + { + try + { + var typeStr = System.Configuration.ConfigurationManager.AppSettings["Detector:Type"]; + if (!string.Equals(typeStr, "Simulated", StringComparison.OrdinalIgnoreCase)) + return; + + Log.Information("[SimulatedDetector] 检测到 Simulated 类型,开始自动初始化探测器..."); + + var detectorService = Container.Resolve(); + + var initResult = await detectorService.InitializeAsync(); + if (!initResult.IsSuccess) + { + Log.Error("SimulatedDetector 自动初始化失败:{Msg}", initResult.ErrorMessage); + return; + } + Log.Information("[SimulatedDetector] 初始化成功,启动连续采集..."); + + var startResult = await detectorService.StartAcquisitionAsync(); + if (!startResult.IsSuccess) + { + Log.Error("SimulatedDetector 启动采集失败:{Msg}", startResult.ErrorMessage); + return; + } + Log.Information("[SimulatedDetector] 连续采集已启动,帧将以 5 fps 持续发布"); + } + catch (Exception ex) + { + Log.Error(ex, "SimulatedDetector 自动启动异常"); + } + } + /// /// 在主线程上检索并连接导航相机。 /// pylon SDK 要求在主线程(STA)上操作,不能放到后台线程。 diff --git a/XplorePlane/Services/Cnc/CncExecutionService.cs b/XplorePlane/Services/Cnc/CncExecutionService.cs index 42f4448..4e247bb 100644 --- a/XplorePlane/Services/Cnc/CncExecutionService.cs +++ b/XplorePlane/Services/Cnc/CncExecutionService.cs @@ -586,20 +586,49 @@ namespace XplorePlane.Services.Cnc private BitmapSource TryGetSourceImage() { - var viewportImage = _mainViewportService?.LatestManualImage as BitmapSource - ?? _mainViewportService?.CurrentDisplayImage as BitmapSource; - if (viewportImage != null) - return viewportImage; + // ── 优先级 1:MainViewportService 中的手动图像或当前显示图像 ── + var manualImage = _mainViewportService?.LatestManualImage as BitmapSource; + if (manualImage != null) + { + _logger.ForModule().Info( + "[图像链路] TryGetSourceImage:从 MainViewportService.LatestManualImage 获取图像,尺寸 {W}x{H}", + manualImage.PixelWidth, manualImage.PixelHeight); + return manualImage; + } + var displayImage = _mainViewportService?.CurrentDisplayImage as BitmapSource; + if (displayImage != null) + { + _logger.ForModule().Info( + "[图像链路] TryGetSourceImage:从 MainViewportService.CurrentDisplayImage 获取图像,尺寸 {W}x{H}", + displayImage.PixelWidth, displayImage.PixelHeight); + return displayImage; + } + + // ── 优先级 2:AppStateService 中的原始探测器帧 ── var detectorFrame = _appStateService?.LatestDetectorFrame; if (detectorFrame?.ImageData == null || detectorFrame.Width <= 0 || detectorFrame.Height <= 0) + { + _logger.ForModule().Warn( + "[图像链路] TryGetSourceImage:所有图像源均为空(MainViewport 无图像,AppState 无探测器帧),返回 null"); return null; + } + + _logger.ForModule().Info( + "[图像链路] TryGetSourceImage:从 AppStateService.LatestDetectorFrame 获取原始帧,帧号 {N},分辨率 {W}x{H},采集时间 {T}", + detectorFrame.FrameNumber, detectorFrame.Width, detectorFrame.Height, + detectorFrame.CaptureTime.ToString("HH:mm:ss.fff")); var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource( detectorFrame.ImageData, (int)detectorFrame.Width, (int)detectorFrame.Height); bitmap.Freeze(); + + _logger.ForModule().Info( + "[图像链路] TryGetSourceImage:Gray16→BitmapSource 转换完成,输出尺寸 {W}x{H}", + bitmap.PixelWidth, bitmap.PixelHeight); + return bitmap; } diff --git a/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs b/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs index ab9f3e5..03c70f5 100644 --- a/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs +++ b/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs @@ -77,6 +77,12 @@ namespace XplorePlane.Services.MainViewport try { + _logger.Info( + "[图像链路] DetectorFramePipeline 收到帧 #{N},分辨率 {W}x{H},采集时间 {T},当前队列深度 AcquireQ={AQ} ProcessQ={PQ}", + args.FrameNumber, args.Width, args.Height, + args.CaptureTime.ToString("HH:mm:ss.fff"), + AcquireQueueCount, ProcessQueueCount); + var rawPixels = new ushort[args.ImageData.Length]; Array.Copy(args.ImageData, rawPixels, rawPixels.Length); @@ -99,6 +105,9 @@ namespace XplorePlane.Services.MainViewport { EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount); _processSignal.Release(); + _logger.Info( + "[图像链路] 帧 #{N} 已入处理队列(每 {Every} 帧采样一次),累计接收 {Total} 帧", + args.FrameNumber, ProcessEveryNFrames, sequence); } } catch (Exception ex) diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index 394b969..1bc835a 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -127,7 +127,8 @@ Margin="4,10,4,4" HorizontalAlignment="Center" ToolTip="主界面实时" - IsChecked="True" + IsChecked="{Binding IsMainViewportRealtimeEnabled, Mode=TwoWay}" + IsEnabled="{Binding IsMainViewportSwitchEnabled}" SwitchWidth="44" SwitchHeight="24" />