Merge branch 'Develop/XP' into turbo-002-cnc

This commit is contained in:
zhengxuan.zhang
2026-04-22 16:09:56 +08:00
49 changed files with 3831 additions and 308 deletions
+73 -8
View File
@@ -13,6 +13,7 @@ using XP.Camera;
using XP.Common.Configs;
using XP.Common.Database.Implementations;
using XP.Common.Database.Interfaces;
using XP.Common.GeneralForm.Views;
using XP.Common.Dump.Configs;
using XP.Common.Dump.Implementations;
using XP.Common.Dump.Interfaces;
@@ -150,7 +151,23 @@ namespace XplorePlane
Log.Error(ex, "射线源资源释放失败");
}
// 释放相机服务资源
// 先停止导航相机实时采集,再释放资源,避免回调死锁
try
{
var bootstrapper = AppBootstrapper.Instance;
if (bootstrapper != null)
{
var cameraVm = bootstrapper.Container.Resolve<NavigationPropertyPanelViewModel>();
cameraVm?.Dispose();
Log.Information("导航相机 ViewModel 已释放");
}
}
catch (Exception ex)
{
Log.Error(ex, "导航相机 ViewModel 释放失败");
}
// 释放导航相机服务资源
try
{
var bootstrapper = AppBootstrapper.Instance;
@@ -158,12 +175,12 @@ namespace XplorePlane
{
var cameraService = bootstrapper.Container.Resolve<ICameraService>();
cameraService?.Dispose();
Log.Information("相机服务资源已释放");
Log.Information("导航相机服务资源已释放");
}
}
catch (Exception ex)
{
Log.Error(ex, "相机服务资源释放失败");
Log.Error(ex, "导航相机服务资源释放失败");
}
// 释放SQLite数据库资源 | Release SQLite database resources
@@ -232,19 +249,67 @@ namespace XplorePlane
private bool _modulesInitialized = false;
private string? _cameraError;
protected override Window CreateShell()
{
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
// 默认 Prism 顺序是 CreateShell → InitializeModules
// 但 MainWindow 中嵌入的硬件控件会在 XAML 解析时触发 ViewModelLocator
// 此时模块尚未加载,导致依赖解析失败
if (!_modulesInitialized)
{
base.InitializeModules();
_modulesInitialized = true;
}
return Container.Resolve<MainWindow>();
var shell = Container.Resolve<MainWindow>();
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
shell.Loaded += (s, e) =>
{
TryConnectCamera();
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
try
{
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
cameraVm.OnCameraReady();
}
catch (Exception ex)
{
Log.Error(ex, "通知相机 ViewModel 失败");
}
if (_cameraError != null)
{
HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
}
};
return shell;
}
/// <summary>
/// 在主线程上检索并连接导航相机。
/// pylon SDK 要求在主线程(STA)上操作,不能放到后台线程。
/// </summary>
private void TryConnectCamera()
{
var camera = Container.Resolve<ICameraController>();
try
{
var info = camera.Open();
Log.Information("导航相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber);
}
catch (DeviceNotFoundException)
{
Log.Warning("未检测到导航相机");
_cameraError = "未检测到导航相机,请检查连接后重启软件。";
}
catch (Exception ex)
{
Log.Warning(ex, "导航相机自动连接失败: {Message}", ex.Message);
_cameraError = $"导航相机连接失败: {ex.Message}";
}
}
/// <summary>
@@ -329,7 +394,7 @@ namespace XplorePlane
containerRegistry.RegisterForNavigation<CncPageView>();
containerRegistry.RegisterForNavigation<MatrixPageView>();
// ── 相机服务(单例)──
// ── 导航相机服务(单例)──
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
containerRegistry.RegisterSingleton<ICameraController>(() =>
new CameraFactory().CreateController("Basler"));
@@ -28,11 +28,11 @@ namespace XplorePlane.Services
var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0);
int width = formatted.PixelWidth;
int height = formatted.PixelHeight;
int stride = width;
byte[] pixels = new byte[height * stride];
formatted.CopyPixels(pixels, stride, 0);
var image = new Image<Gray, byte>(width, height);
int stride = image.Bytes.Length / height;
var pixels = new byte[height * stride];
formatted.CopyPixels(pixels, stride, 0);
image.Bytes = pixels;
return image;
}
@@ -40,7 +40,19 @@ namespace XplorePlane.Services
public static Image<Gray, byte> ToEmguCVFromPixels(byte[] pixels, int width, int height)
{
var image = new Image<Gray, byte>(width, height);
image.Bytes = pixels;
int required = image.Bytes.Length;
if (pixels.Length == required)
{
image.Bytes = pixels;
}
else
{
int stride = required / height;
var padded = new byte[required];
for (int row = 0; row < height; row++)
Buffer.BlockCopy(pixels, row * width, padded, row * stride, width);
image.Bytes = padded;
}
return image;
}
@@ -50,8 +62,8 @@ namespace XplorePlane.Services
int width = emguImage.Width;
int height = emguImage.Height;
int stride = width;
byte[] pixels = emguImage.Bytes;
int stride = pixels.Length / height;
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
}
@@ -176,6 +176,30 @@ namespace XplorePlane.ViewModels
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
CameraStatusText = "正在检索相机...";
}
/// <summary>
/// 相机连接完成后由外部调用,启动实时预览。
/// </summary>
public void OnCameraReady()
{
if (!_camera.IsConnected)
{
CameraStatusText = "未检测到相机";
return;
}
_camera.ImageGrabbed += OnCameraImageGrabbed;
_camera.GrabError += OnCameraGrabError;
_camera.ConnectionLost += OnCameraConnectionLost;
IsCameraConnected = true;
CameraStatusText = "已连接";
RefreshCameraParams();
StartGrab();
IsLiveViewEnabled = true;
}
#region Camera Methods
@@ -326,15 +350,18 @@ namespace XplorePlane.ViewModels
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
{
if (_disposed) return;
try
{
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
var app = Application.Current;
if (app == null) return;
app.Dispatcher.Invoke(() =>
app.Dispatcher.BeginInvoke(() =>
{
CameraImageSource = bitmap;
if (!_disposed)
CameraImageSource = bitmap;
});
if (_liveViewRunning)
@@ -344,7 +371,8 @@ namespace XplorePlane.ViewModels
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to process camera image");
if (!_disposed)
_logger.Error(ex, "Failed to process camera image");
}
}
@@ -354,9 +382,10 @@ namespace XplorePlane.ViewModels
var app = Application.Current;
if (app == null) return;
app.Dispatcher.Invoke(() =>
app.Dispatcher.BeginInvoke(() =>
{
CameraStatusText = $"采集错误: {e.ErrorDescription}";
if (!_disposed)
CameraStatusText = $"采集错误: {e.ErrorDescription}";
});
}
@@ -366,8 +395,9 @@ namespace XplorePlane.ViewModels
var app = Application.Current;
if (app == null) return;
app.Dispatcher.Invoke(() =>
app.Dispatcher.BeginInvoke(() =>
{
if (_disposed) return;
IsCameraConnected = false;
IsCameraGrabbing = false;
CameraStatusText = "连接已断开";
@@ -382,12 +412,19 @@ namespace XplorePlane.ViewModels
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_liveViewRunning = false;
try { _camera.Dispose(); }
catch (Exception ex) { _logger.Error(ex, "Error disposing camera"); }
// 先取消事件订阅,防止回调继续触发
_camera.ImageGrabbed -= OnCameraImageGrabbed;
_camera.GrabError -= OnCameraGrabError;
_camera.ConnectionLost -= OnCameraConnectionLost;
_disposed = true;
// 停止采集后再关闭连接
try { if (_camera.IsGrabbing) _camera.StopGrabbing(); } catch { }
try { _camera.Close(); } catch { }
_logger.Information("NavigationPropertyPanelViewModel disposed");
}
#endregion IDisposable
@@ -12,7 +12,6 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 标题栏 -->
@@ -56,50 +55,5 @@
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
</TextBlock>
</Border>
<!-- 控制按钮栏 -->
<Border
Grid.Row="3"
Background="#F0F0F0"
BorderBrush="#DDDDDD"
BorderThickness="0,1,0,0"
Padding="4">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button
Command="{Binding ConnectCameraCommand}"
Width="60" Height="26"
Margin="2"
Background="#4CAF50"
Foreground="#000000"
Content="连接" />
<Button
Command="{Binding DisconnectCameraCommand}"
Width="60" Height="26"
Margin="2"
Background="#F44336"
Foreground="#000000"
Content="断开" />
<Button
Command="{Binding StartGrabCommand}"
Width="60" Height="26"
Margin="2"
Background="#2196F3"
Foreground="#000000"
Content="采集" />
<Button
Command="{Binding StopGrabCommand}"
Width="60" Height="26"
Margin="2"
Background="#FF9800"
Foreground="#000000"
Content="停止" />
<CheckBox
IsChecked="{Binding IsLiveViewEnabled}"
VerticalAlignment="Center"
Margin="6,0,0,0"
Foreground="#333333"
Content="实时" />
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -28,8 +28,16 @@ namespace XplorePlane.Views
}
}
/// <summary>
/// 双击相机图像时,计算并显示点击位置的像素坐标。
/// </summary>
/// <remarks>
/// TODO: 后续需要将点击的像素坐标通过 CalibrationProcessor 转换为世界坐标,
/// 再传给运动机构执行定位。
/// </remarks>
private void ImgCamera_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount != 2) return;
if (_viewModel?.CameraImageSource == null) return;
var image = (Image)sender;