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" />