From e9d388beb25e05e08ad38d126287995e4d964e07 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Fri, 15 May 2026 16:51:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=99=9A=E6=8B=9F=E6=8E=A2=E6=B5=8B=E5=99=A8?= =?UTF-8?q?=E4=B8=8E=E5=AE=9E=E6=97=B6=E5=88=87=E6=8D=A2=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=9A=84=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DataBase/XP.db | Bin 176128 -> 192512 bytes .../Abstractions/Enums/DetectorType.cs | 7 ++- XP.Hardware.Detector/Config/ConfigLoader.cs | 42 +++++++++++++---- .../Factories/DetectorFactory.cs | 18 ++++++- .../XP.Hardware.Detector.deps.json | 5 +- XplorePlane/App.config | 7 ++- XplorePlane/App.xaml.cs | 44 +++++++++++++++++- .../Services/Cnc/CncExecutionService.cs | 37 +++++++++++++-- .../DetectorFramePipelineService.cs | 9 ++++ XplorePlane/Views/Main/MainWindow.xaml | 3 +- 10 files changed, 153 insertions(+), 19 deletions(-) diff --git a/DataBase/XP.db b/DataBase/XP.db index c79ddc077dea6be02667e8bab2f88a307a803326..f9d8e0779aadcba61755804d214970cf729a2f38 100644 GIT binary patch delta 14925 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 delta 1107 zcmc(eU2IfE7=~xQbM~Aw=giOPK~6Ph-Ig{KDXmNELM=uLVwFe_5Z%}dOu@t`UYJ&m zMjK>z)g}^u*lfr|#L%Q9dqHc#6PZBxi;&*9q(-6;6R_1pLn?bi4r*mbm zROgS%@x!np9(iY%VT7!?Wbbc@Yy1UTDj#BT$43<~LT;Dr;VQk%N=L`SIQj99*a$_b zWFM>2KHZ%g_!pQQaIsj_22hD9``Li$iAgz?~I7O zdh#KiV@2wT+IY>>oE6vkX)t=stUP`gmdmao+!!Cc@tpPF9rE~qjjO8na@oppjk?Hj z*Y>})E)$ENa=0m$YLWG|peq&|bw!P0V=BZ1l(XB4t*5P8@;TWG{$cP%k&JgMw2@b9URK!|uBJ?kaib9hE+I= ze@+^{IVx=*>NR`QRHL^+)?4fCLrYPF?1~*&5vh>CiP&e%p_`0~5kTZQZj`7b0yDxD z9H5AA^7e@wpD1n!Rn(6Ol*w{{@)Iv%P^1+W@QGER+pVwRb=c$+=#+eK5UVm?2_W$>$UbY|Zxt*^JYr6WZeHMsb<+YxUbkCt5$SdcTJRxLR$jqx zsLdI;&rs0|`e}9P8~D{wTVKX|@QE%keQHvUgvBWfDA#xX@D$$x^M-4X8N_aRvt&lo zYedcLhC3`PJ_luF;67 W{^hJj(@0E<*|;YCm5|fpkopUz5Hs=s 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" />