TURBO-568:修复PLC连接和相机连接时序问题导致的内存访问异常
This commit is contained in:
+53
-26
@@ -150,6 +150,22 @@ namespace XplorePlane
|
|||||||
Log.Error(ex, "射线源资源释放失败");
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -232,6 +248,8 @@ namespace XplorePlane
|
|||||||
|
|
||||||
private bool _modulesInitialized = false;
|
private bool _modulesInitialized = false;
|
||||||
|
|
||||||
|
private string? _cameraError;
|
||||||
|
|
||||||
protected override Window CreateShell()
|
protected override Window CreateShell()
|
||||||
{
|
{
|
||||||
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
|
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
|
||||||
@@ -241,46 +259,55 @@ namespace XplorePlane
|
|||||||
_modulesInitialized = true;
|
_modulesInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在主窗口显示前完成导航相机连接,避免窗口弹出后再检索导致卡顿
|
var shell = Container.Resolve<MainWindow>();
|
||||||
TryConnectCamera();
|
|
||||||
|
|
||||||
return 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>
|
/// <summary>
|
||||||
/// 在独立 MTA 线程中检索并连接导航相机,带 10 秒超时。
|
/// 在主线程上检索并连接导航相机。
|
||||||
/// 连接成功后 NavigationPropertyPanelViewModel 构造时拿到的就是已连接的导航相机。
|
/// pylon SDK 要求在主线程(STA)上操作,不能放到后台线程。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void TryConnectCamera()
|
private void TryConnectCamera()
|
||||||
{
|
{
|
||||||
var camera = Container.Resolve<ICameraController>();
|
var camera = Container.Resolve<ICameraController>();
|
||||||
CameraInfo? info = null;
|
|
||||||
Exception? error = null;
|
|
||||||
|
|
||||||
var thread = new System.Threading.Thread(() =>
|
try
|
||||||
{
|
{
|
||||||
try { info = camera.Open(); }
|
var info = camera.Open();
|
||||||
catch (Exception ex) { error = ex; }
|
Log.Information("导航相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber);
|
||||||
});
|
|
||||||
thread.IsBackground = true;
|
|
||||||
thread.SetApartmentState(System.Threading.ApartmentState.MTA);
|
|
||||||
thread.Start();
|
|
||||||
|
|
||||||
bool completed = thread.Join(TimeSpan.FromSeconds(10));
|
|
||||||
|
|
||||||
if (!completed)
|
|
||||||
{
|
|
||||||
Log.Warning("导航相机检索超时 (10s),跳过自动连接");
|
|
||||||
HexMessageBox.Show("导航相机检索超时(10秒),请检查导航相机连接后重启软件。", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
else if (error != null)
|
catch (DeviceNotFoundException)
|
||||||
{
|
{
|
||||||
Log.Warning(error, "导航相机自动连接失败: {Message}", error.Message);
|
Log.Warning("未检测到导航相机");
|
||||||
HexMessageBox.Show($"导航相机连接失败: {error.Message}", MessageBoxButton.OK, MessageBoxImage.Error);
|
_cameraError = "未检测到导航相机,请检查连接后重启软件。";
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Information("导航相机已连接: {ModelName} (SN: {SerialNumber})", info!.ModelName, info.SerialNumber);
|
Log.Warning(ex, "导航相机自动连接失败: {Message}", ex.Message);
|
||||||
|
_cameraError = $"导航相机连接失败: {ex.Message}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,23 +177,29 @@ namespace XplorePlane.ViewModels
|
|||||||
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
||||||
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
||||||
|
|
||||||
// 相机已在 App 启动阶段连接,直接检测状态并启动实时预览
|
CameraStatusText = "正在检索相机...";
|
||||||
if (_camera.IsConnected)
|
}
|
||||||
{
|
|
||||||
_camera.ImageGrabbed += OnCameraImageGrabbed;
|
|
||||||
_camera.GrabError += OnCameraGrabError;
|
|
||||||
_camera.ConnectionLost += OnCameraConnectionLost;
|
|
||||||
|
|
||||||
IsCameraConnected = true;
|
/// <summary>
|
||||||
CameraStatusText = "已连接";
|
/// 相机连接完成后由外部调用,启动实时预览。
|
||||||
RefreshCameraParams();
|
/// </summary>
|
||||||
StartGrab();
|
public void OnCameraReady()
|
||||||
IsLiveViewEnabled = true;
|
{
|
||||||
}
|
if (!_camera.IsConnected)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
CameraStatusText = "未检测到相机";
|
CameraStatusText = "未检测到相机";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_camera.ImageGrabbed += OnCameraImageGrabbed;
|
||||||
|
_camera.GrabError += OnCameraGrabError;
|
||||||
|
_camera.ConnectionLost += OnCameraConnectionLost;
|
||||||
|
|
||||||
|
IsCameraConnected = true;
|
||||||
|
CameraStatusText = "已连接";
|
||||||
|
RefreshCameraParams();
|
||||||
|
StartGrab();
|
||||||
|
IsLiveViewEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Camera Methods
|
#region Camera Methods
|
||||||
@@ -344,15 +350,18 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
if (app == null) return;
|
if (app == null) return;
|
||||||
|
|
||||||
app.Dispatcher.Invoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
CameraImageSource = bitmap;
|
if (!_disposed)
|
||||||
|
CameraImageSource = bitmap;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_liveViewRunning)
|
if (_liveViewRunning)
|
||||||
@@ -362,7 +371,8 @@ namespace XplorePlane.ViewModels
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to process camera image");
|
if (!_disposed)
|
||||||
|
_logger.Error(ex, "Failed to process camera image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,9 +382,10 @@ namespace XplorePlane.ViewModels
|
|||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
if (app == null) return;
|
if (app == null) return;
|
||||||
|
|
||||||
app.Dispatcher.Invoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
CameraStatusText = $"采集错误: {e.ErrorDescription}";
|
if (!_disposed)
|
||||||
|
CameraStatusText = $"采集错误: {e.ErrorDescription}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,8 +395,9 @@ namespace XplorePlane.ViewModels
|
|||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
if (app == null) return;
|
if (app == null) return;
|
||||||
|
|
||||||
app.Dispatcher.Invoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
|
if (_disposed) return;
|
||||||
IsCameraConnected = false;
|
IsCameraConnected = false;
|
||||||
IsCameraGrabbing = false;
|
IsCameraGrabbing = false;
|
||||||
CameraStatusText = "连接已断开";
|
CameraStatusText = "连接已断开";
|
||||||
@@ -400,12 +412,19 @@ namespace XplorePlane.ViewModels
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
_liveViewRunning = false;
|
_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
|
#endregion IDisposable
|
||||||
|
|||||||
Reference in New Issue
Block a user