Files
XplorePlane/XP.Camera/Basler/BaslerCameraController.cs
T
2026-04-21 16:00:36 +08:00

513 lines
16 KiB
C#

using Basler.Pylon;
using Serilog;
using CameraGrabErrorEventArgs = XP.Camera.GrabErrorEventArgs;
using CameraImageGrabbedEventArgs = XP.Camera.ImageGrabbedEventArgs;
namespace XP.Camera;
/// <summary>
/// Basler 相机控制器,封装 Basler pylon .NET SDK 实现 <see cref="ICameraController"/>。
/// </summary>
/// <remarks>
/// <para>所有公共方法通过内部 <c>_syncLock</c> 对象进行 lock 同步,保证线程安全。</para>
/// <para>事件回调(ImageGrabbed、GrabError)在 StreamGrabber 回调线程上触发,不持有 _syncLock,避免死锁。</para>
/// <para>ConnectionLost 事件在 pylon SDK 事件线程上触发。</para>
/// </remarks>
public class BaslerCameraController : ICameraController
{
private static readonly ILogger _logger = Log.ForContext<BaslerCameraController>();
private readonly object _syncLock = new();
private Basler.Pylon.Camera? _camera;
private CameraInfo? _cachedCameraInfo;
private bool _isConnected;
private bool _isGrabbing;
/// <summary>
/// 初始化 <see cref="BaslerCameraController"/> 实例。
/// </summary>
public BaslerCameraController()
{
}
/// <inheritdoc />
public bool IsConnected
{
get { lock (_syncLock) { return _isConnected; } }
}
/// <inheritdoc />
public bool IsGrabbing
{
get { lock (_syncLock) { return _isGrabbing; } }
}
/// <inheritdoc />
public event EventHandler<CameraImageGrabbedEventArgs>? ImageGrabbed;
/// <inheritdoc />
public event EventHandler<CameraGrabErrorEventArgs>? GrabError;
/// <inheritdoc />
public event EventHandler? ConnectionLost;
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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();
}
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);
}
}
}
/// <inheritdoc />
public void StopGrabbing()
{
lock (_syncLock)
{
if (!_isGrabbing)
{
return;
}
StopGrabbingInternal();
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
public void Dispose()
{
Close();
GC.SuppressFinalize(this);
}
/// <summary>
/// StreamGrabber.ImageGrabbed 事件处理。在 StreamGrabber 回调线程上调用,不持有 _syncLock。
/// </summary>
/// <remarks>
/// <para>当图像采集成功时,提取像素数据、宽高和像素格式,触发 <see cref="ImageGrabbed"/> 事件。</para>
/// <para>当图像采集失败时,提取错误码和错误描述,触发 <see cref="GrabError"/> 事件。</para>
/// <para>此方法在 StreamGrabber 回调线程上执行,不持有 _syncLock,以避免死锁。
/// 调用方如需在 WPF UI 线程上处理事件,应自行通过 Dispatcher 调度。</para>
/// </remarks>
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<byte>();
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.");
}
}
/// <summary>
/// ConnectionLost 事件处理。在 pylon SDK 事件线程上调用。
/// </summary>
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);
}
/// <summary>
/// Internal stop grabbing without lock (caller must hold _syncLock).
/// </summary>
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);
}
}
/// <summary>
/// Throws <see cref="InvalidOperationException"/> if the camera is not connected.
/// Must be called within a lock on <see cref="_syncLock"/>.
/// </summary>
private void EnsureConnected()
{
if (!_isConnected)
{
throw new InvalidOperationException("Camera is not connected. Call Open() first.");
}
}
}