using Basler.Pylon; using Serilog; using CameraGrabErrorEventArgs = XP.Camera.GrabErrorEventArgs; using CameraImageGrabbedEventArgs = XP.Camera.ImageGrabbedEventArgs; namespace XP.Camera; /// /// Basler 相机控制器,封装 Basler pylon .NET SDK 实现 。 /// /// /// 所有公共方法通过内部 _syncLock 对象进行 lock 同步,保证线程安全。 /// 事件回调(ImageGrabbed、GrabError)在 StreamGrabber 回调线程上触发,不持有 _syncLock,避免死锁。 /// ConnectionLost 事件在 pylon SDK 事件线程上触发。 /// public class BaslerCameraController : ICameraController { private static readonly ILogger _logger = Log.ForContext(); private readonly object _syncLock = new(); private Basler.Pylon.Camera? _camera; private CameraInfo? _cachedCameraInfo; private bool _isConnected; private bool _isGrabbing; /// /// 初始化 实例。 /// public BaslerCameraController() { } /// public bool IsConnected { get { lock (_syncLock) { return _isConnected; } } } /// public bool IsGrabbing { get { lock (_syncLock) { return _isGrabbing; } } } /// public event EventHandler? ImageGrabbed; /// public event EventHandler? GrabError; /// public event EventHandler? ConnectionLost; /// public CameraInfo Open() { lock (_syncLock) { if (_isConnected && _cachedCameraInfo != null) { _logger.Information("Camera already connected, returning cached info."); return _cachedCameraInfo; } try { _logger.Information("Opening camera connection..."); _camera = new Basler.Pylon.Camera(CameraSelectionStrategy.FirstFound); _camera.CameraOpened += (sender, e) => Configuration.SoftwareTrigger(sender!, e); _camera.ConnectionLost += OnConnectionLost; _camera.Open(); _cachedCameraInfo = new CameraInfo( ModelName: _camera.CameraInfo![CameraInfoKey.ModelName] ?? "", SerialNumber: _camera.CameraInfo[CameraInfoKey.SerialNumber] ?? "", VendorName: _camera.CameraInfo[CameraInfoKey.VendorName] ?? "", DeviceType: _camera.CameraInfo[CameraInfoKey.DeviceType] ?? "" ); _isConnected = true; _logger.Information("Camera connected: {ModelName} (SN: {SerialNumber})", _cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber); return _cachedCameraInfo; } catch (Exception ex) when (ex is not CameraException) { // Clean up partially created camera on failure _camera?.Dispose(); _camera = null; if (ex.Message.Contains("No device", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase) || ex is InvalidOperationException) { _logger.Error(ex, "No camera device found."); throw new DeviceNotFoundException("No Basler camera device found.", ex); } _logger.Error(ex, "Failed to open camera."); throw new CameraException("Failed to open camera device.", ex); } } } /// public void Close() { lock (_syncLock) { if (!_isConnected) { _logger.Information("Camera not connected, Close() ignored."); return; } try { if (_isGrabbing) { StopGrabbingInternal(); } _logger.Information("Closing camera connection..."); _camera?.Close(); _camera?.Dispose(); _camera = null; _isConnected = false; _cachedCameraInfo = null; _logger.Information("Camera connection closed."); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Error while closing camera."); // Still clean up state even if close fails _camera = null; _isConnected = false; _isGrabbing = false; _cachedCameraInfo = null; throw new CameraException("Failed to close camera device.", ex); } } } /// public void StartGrabbing() { lock (_syncLock) { EnsureConnected(); if (_isGrabbing) { _logger.Information("Already grabbing, StartGrabbing() ignored."); return; } try { _logger.Information("Starting grabbing with software trigger..."); // Register ImageReady event for grab results (task 6.2 will implement the handler) _camera!.StreamGrabber!.ImageGrabbed += OnImageGrabbed; // Start grabbing using the grab loop thread provided by StreamGrabber _camera.StreamGrabber!.Start(GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber); _isGrabbing = true; _logger.Information("Grabbing started."); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to start grabbing."); throw new CameraException("Failed to start grabbing.", ex); } } } /// public void ExecuteSoftwareTrigger() { lock (_syncLock) { if (!_isGrabbing) { throw new InvalidOperationException("Cannot execute software trigger: camera is not grabbing."); } try { // Wait until the camera is ready to accept the next frame trigger if (!_camera!.WaitForFrameTriggerReady(1000, TimeoutHandling.Return)) { throw new TimeoutException("Camera was not ready for frame trigger within 1000 ms."); } _camera.ExecuteSoftwareTrigger(); _logger.Debug("Software trigger executed."); } catch (TimeoutException) { throw; // Re-throw our own TimeoutException } catch (Exception ex) when (ex is not CameraException and not InvalidOperationException) { _logger.Error(ex, "Failed to execute software trigger."); throw new CameraException("Failed to execute software trigger.", ex); } } } /// public void StopGrabbing() { lock (_syncLock) { if (!_isGrabbing) { return; } StopGrabbingInternal(); } } /// public double GetExposureTime() { lock (_syncLock) { EnsureConnected(); try { return _camera!.Parameters[PLCamera.ExposureTime].GetValue(); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to get exposure time."); throw new CameraException("Failed to get exposure time.", ex); } } } /// public void SetExposureTime(double microseconds) { lock (_syncLock) { EnsureConnected(); try { _camera!.Parameters[PLCamera.ExposureTime].SetValue(microseconds); _logger.Information("Exposure time set to {Microseconds} µs.", microseconds); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to set exposure time to {Microseconds} µs.", microseconds); throw new CameraException("Failed to set exposure time.", ex); } } } /// public double GetGain() { lock (_syncLock) { EnsureConnected(); try { return _camera!.Parameters[PLCamera.Gain].GetValue(); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to get gain."); throw new CameraException("Failed to get gain.", ex); } } } /// public void SetGain(double value) { lock (_syncLock) { EnsureConnected(); try { _camera!.Parameters[PLCamera.Gain].SetValue(value); _logger.Information("Gain set to {Value}.", value); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to set gain to {Value}.", value); throw new CameraException("Failed to set gain.", ex); } } } /// public int GetWidth() { lock (_syncLock) { EnsureConnected(); try { return (int)_camera!.Parameters[PLCamera.Width].GetValue(); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to get width."); throw new CameraException("Failed to get width.", ex); } } } /// public void SetWidth(int value) { lock (_syncLock) { EnsureConnected(); try { _camera!.Parameters[PLCamera.Width].SetValue(value, IntegerValueCorrection.Nearest); _logger.Information("Width set to {Value}.", value); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to set width to {Value}.", value); throw new CameraException("Failed to set width.", ex); } } } /// public int GetHeight() { lock (_syncLock) { EnsureConnected(); try { return (int)_camera!.Parameters[PLCamera.Height].GetValue(); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to get height."); throw new CameraException("Failed to get height.", ex); } } } /// public void SetHeight(int value) { lock (_syncLock) { EnsureConnected(); try { _camera!.Parameters[PLCamera.Height].SetValue(value, IntegerValueCorrection.Nearest); _logger.Information("Height set to {Value}.", value); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to set height to {Value}.", value); throw new CameraException("Failed to set height.", ex); } } } /// public string GetPixelFormat() { lock (_syncLock) { EnsureConnected(); try { return _camera!.Parameters[PLCamera.PixelFormat].GetValue(); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to get pixel format."); throw new CameraException("Failed to get pixel format.", ex); } } } /// public void SetPixelFormat(string format) { lock (_syncLock) { EnsureConnected(); try { _camera!.Parameters[PLCamera.PixelFormat].SetValue(format); _logger.Information("Pixel format set to {Format}.", format); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to set pixel format to {Format}.", format); throw new CameraException("Failed to set pixel format.", ex); } } } /// public void Dispose() { Close(); GC.SuppressFinalize(this); } /// /// StreamGrabber.ImageGrabbed 事件处理。在 StreamGrabber 回调线程上调用,不持有 _syncLock。 /// /// /// 当图像采集成功时,提取像素数据、宽高和像素格式,触发 事件。 /// 当图像采集失败时,提取错误码和错误描述,触发 事件。 /// 此方法在 StreamGrabber 回调线程上执行,不持有 _syncLock,以避免死锁。 /// 调用方如需在 WPF UI 线程上处理事件,应自行通过 Dispatcher 调度。 /// private void OnImageGrabbed(object? sender, Basler.Pylon.ImageGrabbedEventArgs e) { try { IGrabResult grabResult = e.GrabResult; if (grabResult.GrabSucceeded) { byte[] pixelData = grabResult.PixelData as byte[] ?? Array.Empty(); int width = grabResult.Width; int height = grabResult.Height; string pixelFormat = grabResult.PixelTypeValue.ToString(); var args = new CameraImageGrabbedEventArgs(pixelData, width, height, pixelFormat); ImageGrabbed?.Invoke(this, args); } else { int errorCode = (int)grabResult.ErrorCode; string errorDescription = grabResult.ErrorDescription ?? "Unknown grab error."; _logger.Error("Image grab failed. ErrorCode: {ErrorCode}, Description: {ErrorDescription}", errorCode, errorDescription); var args = new CameraGrabErrorEventArgs(errorCode, errorDescription); GrabError?.Invoke(this, args); } } catch (Exception ex) { _logger.Error(ex, "Exception in OnImageGrabbed handler."); } } /// /// ConnectionLost 事件处理。在 pylon SDK 事件线程上调用。 /// private void OnConnectionLost(object? sender, EventArgs e) { _logger.Warning("Camera connection lost."); lock (_syncLock) { _isGrabbing = false; _isConnected = false; _cachedCameraInfo = null; } // Raise event outside lock to avoid deadlock ConnectionLost?.Invoke(this, EventArgs.Empty); } /// /// Internal stop grabbing without lock (caller must hold _syncLock). /// private void StopGrabbingInternal() { if (!_isGrabbing) return; try { _camera?.StreamGrabber?.Stop(); if (_camera != null) _camera.StreamGrabber!.ImageGrabbed -= OnImageGrabbed; _isGrabbing = false; _logger.Information("Grabbing stopped."); } catch (Exception ex) when (ex is not CameraException) { _isGrabbing = false; _logger.Error(ex, "Error while stopping grabbing."); throw new CameraException("Failed to stop grabbing.", ex); } } /// /// Throws if the camera is not connected. /// Must be called within a lock on . /// private void EnsureConnected() { if (!_isConnected) { throw new InvalidOperationException("Camera is not connected. Call Open() first."); } } }