514 lines
16 KiB
C#
514 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();
|
|
_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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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.");
|
|
}
|
|
}
|
|
} |