using MvCameraControl; using Serilog; namespace XP.Camera; /// /// 海康威视相机控制器,封装 MvCameraControl.Net SDK 实现 。 /// /// /// 所有公共方法通过内部 _syncLock 对象进行 lock 同步,保证线程安全。 /// 事件回调(ImageGrabbed、GrabError)在 SDK 回调线程上触发,不持有 _syncLock,避免死锁。 /// public class HikvisionCameraController : ICameraController { private static readonly ILogger _logger = Log.ForContext(); private static bool _sdkInitialized; private static readonly object _sdkInitLock = new(); private readonly object _syncLock = new(); private IDevice? _device; private CameraInfo? _cachedCameraInfo; private bool _isConnected; private bool _isGrabbing; public HikvisionCameraController() { // SDK 初始化延迟到 Open() 中执行 } /// 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("Hikvision camera already connected, returning cached info."); return _cachedCameraInfo; } try { _logger.Information("Opening Hikvision camera connection..."); // 确保 SDK 初始化 EnsureSdkInitialized(); // 枚举设备 DeviceTLayerType layerType = DeviceTLayerType.MvGigEDevice | DeviceTLayerType.MvUsbDevice; List deviceInfoList; int ret = DeviceEnumerator.EnumDevices(layerType, out deviceInfoList); _logger.Information("EnumDevices(GigE|USB) returned: 0x{RetCode:X8}, device count: {Count}", ret, deviceInfoList?.Count ?? 0); // 如果没找到,分别尝试 if (ret == MvError.MV_OK && (deviceInfoList == null || deviceInfoList.Count == 0)) { // 单独尝试 GigE List gigeList; int retGige = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvGigEDevice, out gigeList); _logger.Information("EnumDevices(GigE only) returned: 0x{RetCode:X8}, count: {Count}", retGige, gigeList?.Count ?? 0); // 单独尝试 USB List usbList; int retUsb = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvUsbDevice, out usbList); _logger.Information("EnumDevices(USB only) returned: 0x{RetCode:X8}, count: {Count}", retUsb, usbList?.Count ?? 0); // 合并结果 deviceInfoList = new List(); if (gigeList != null) deviceInfoList.AddRange(gigeList); if (usbList != null) deviceInfoList.AddRange(usbList); } if (ret != MvError.MV_OK) { throw new CameraException($"Enumerate Hikvision devices failed: 0x{ret:X8}"); } if (deviceInfoList == null || deviceInfoList.Count == 0) { throw new DeviceNotFoundException("No Hikvision camera device found."); } // 选择第一个设备 IDeviceInfo deviceInfo = deviceInfoList[0]; _logger.Information("Found Hikvision device: {Model} (SN: {Serial})", deviceInfo.ModelName, deviceInfo.SerialNumber); // 创建设备 _device = DeviceFactory.CreateDevice(deviceInfo); // 打开设备 ret = _device.Open(); if (ret != MvError.MV_OK) { _device.Dispose(); _device = null; throw new CameraException($"Open Hikvision device failed: 0x{ret:X8}"); } // GigE 设备优化包大小 if (_device is IGigEDevice gigEDevice) { int packetSize; ret = gigEDevice.GetOptimalPacketSize(out packetSize); if (ret == MvError.MV_OK && packetSize > 0) { _device.Parameters.SetIntValue("GevSCPSPacketSize", packetSize); _logger.Debug("Set GigE packet size to {PacketSize}", packetSize); } } // 配置软件触发模式 _device.Parameters.SetEnumValueByString("TriggerMode", "On"); _device.Parameters.SetEnumValueByString("TriggerSource", "Software"); // 彩色相机:尝试设置输出为 BGR8 以便直接显示 // 如果相机不支持 BGR8(如只支持 Bayer),则保持默认 int fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "BGR8Packed"); if (fmtRet != MvError.MV_OK) { // 尝试 Mono8(黑白相机) fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "Mono8"); } _logger.Debug("Set PixelFormat result: 0x{Ret:X8}", fmtRet); _cachedCameraInfo = new CameraInfo( ModelName: deviceInfo.ModelName ?? "", SerialNumber: deviceInfo.SerialNumber ?? "", VendorName: deviceInfo.ManufacturerName ?? "", DeviceType: deviceInfo.TLayerType.ToString() ); _isConnected = true; _logger.Information("Hikvision camera connected: {ModelName} (SN: {SerialNumber})", _cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber); return _cachedCameraInfo; } catch (Exception ex) when (ex is not CameraException) { _device?.Dispose(); _device = null; _logger.Error(ex, "Failed to open Hikvision camera."); throw new CameraException("Failed to open Hikvision camera device.", ex); } } } /// public void Close() { lock (_syncLock) { if (!_isConnected) { _logger.Information("Hikvision camera not connected, Close() ignored."); return; } try { if (_isGrabbing) { StopGrabbingInternal(); } _logger.Information("Closing Hikvision camera connection..."); _device?.Close(); _device?.Dispose(); _device = null; _isConnected = false; _cachedCameraInfo = null; _logger.Information("Hikvision camera connection closed."); } catch (Exception ex) when (ex is not CameraException) { _device = null; _isConnected = false; _isGrabbing = false; _cachedCameraInfo = null; _logger.Error(ex, "Error while closing Hikvision camera."); throw new CameraException("Failed to close Hikvision camera device.", ex); } } } /// public void StartGrabbing() { lock (_syncLock) { EnsureConnected(); if (_isGrabbing) { _logger.Information("Already grabbing, StartGrabbing() ignored."); return; } try { _logger.Information("Starting Hikvision grabbing with software trigger..."); // 设置缓存节点数 _device!.StreamGrabber.SetImageNodeNum(5); // 注册回调 _device.StreamGrabber.FrameGrabedEvent += OnFrameGrabbed; // 开始采集 int ret = _device.StreamGrabber.StartGrabbing(); if (ret != MvError.MV_OK) { _device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed; throw new CameraException($"Start grabbing failed: 0x{ret:X8}"); } _isGrabbing = true; _logger.Information("Hikvision grabbing started."); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to start Hikvision 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 { int ret = _device!.Parameters.SetCommandValue("TriggerSoftware"); if (ret != MvError.MV_OK) { throw new CameraException($"Execute software trigger failed: 0x{ret:X8}"); } } 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(); IFloatValue floatValue; int ret = _device!.Parameters.GetFloatValue("ExposureTime", out floatValue); if (ret != MvError.MV_OK) throw new CameraException($"Get ExposureTime failed: 0x{ret:X8}"); return floatValue.CurValue; } } /// public void SetExposureTime(double microseconds) { lock (_syncLock) { EnsureConnected(); // 关闭自动曝光 _device!.Parameters.SetEnumValueByString("ExposureAuto", "Off"); int ret = _device.Parameters.SetFloatValue("ExposureTime", (float)microseconds); if (ret != MvError.MV_OK) throw new CameraException($"Set ExposureTime failed: 0x{ret:X8}"); _logger.Information("Hikvision exposure time set to {Microseconds} µs.", microseconds); } } /// public double GetGain() { lock (_syncLock) { EnsureConnected(); IFloatValue floatValue; int ret = _device!.Parameters.GetFloatValue("Gain", out floatValue); if (ret != MvError.MV_OK) throw new CameraException($"Get Gain failed: 0x{ret:X8}"); return floatValue.CurValue; } } /// public void SetGain(double value) { lock (_syncLock) { EnsureConnected(); _device!.Parameters.SetEnumValueByString("GainAuto", "Off"); int ret = _device.Parameters.SetFloatValue("Gain", (float)value); if (ret != MvError.MV_OK) throw new CameraException($"Set Gain failed: 0x{ret:X8}"); _logger.Information("Hikvision gain set to {Value}.", value); } } /// public int GetWidth() { lock (_syncLock) { EnsureConnected(); IIntValue intValue; int ret = _device!.Parameters.GetIntValue("Width", out intValue); if (ret != MvError.MV_OK) throw new CameraException($"Get Width failed: 0x{ret:X8}"); return (int)intValue.CurValue; } } /// public void SetWidth(int value) { lock (_syncLock) { EnsureConnected(); int ret = _device!.Parameters.SetIntValue("Width", value); if (ret != MvError.MV_OK) throw new CameraException($"Set Width failed: 0x{ret:X8}"); _logger.Information("Hikvision width set to {Value}.", value); } } /// public int GetHeight() { lock (_syncLock) { EnsureConnected(); IIntValue intValue; int ret = _device!.Parameters.GetIntValue("Height", out intValue); if (ret != MvError.MV_OK) throw new CameraException($"Get Height failed: 0x{ret:X8}"); return (int)intValue.CurValue; } } /// public void SetHeight(int value) { lock (_syncLock) { EnsureConnected(); int ret = _device!.Parameters.SetIntValue("Height", value); if (ret != MvError.MV_OK) throw new CameraException($"Set Height failed: 0x{ret:X8}"); _logger.Information("Hikvision height set to {Value}.", value); } } /// public string GetPixelFormat() { lock (_syncLock) { EnsureConnected(); IEnumValue enumValue; int ret = _device!.Parameters.GetEnumValue("PixelFormat", out enumValue); if (ret != MvError.MV_OK) throw new CameraException($"Get PixelFormat failed: 0x{ret:X8}"); return enumValue.CurEnumEntry.Symbolic; } } /// public void SetPixelFormat(string format) { lock (_syncLock) { EnsureConnected(); int ret = _device!.Parameters.SetEnumValueByString("PixelFormat", format); if (ret != MvError.MV_OK) throw new CameraException($"Set PixelFormat failed: 0x{ret:X8}"); _logger.Information("Hikvision pixel format set to {Format}.", format); } } /// public void Dispose() { Close(); GC.SuppressFinalize(this); } // ══════════════════════════════════════════════════════════════ // 私有方法 // ══════════════════════════════════════════════════════════════ /// /// SDK 回调:图像采集完成 /// private void OnFrameGrabbed(object? sender, FrameGrabbedEventArgs e) { try { var frameOut = e.FrameOut; if (frameOut == null || frameOut.Image == null) { _logger.Warning("Hikvision OnFrameGrabbed: FrameOut or Image is null"); GrabError?.Invoke(this, new GrabErrorEventArgs(-1, "FrameOut or Image is null.")); return; } var image = frameOut.Image; int width = (int)image.Width; int height = (int)image.Height; int imageSize = (int)image.ImageSize; string pixelFormat = image.PixelType.ToString(); // 提取像素数据 byte[] pixelData = image.PixelData ?? Array.Empty(); _logger.Debug("Hikvision frame: {Width}x{Height}, format={Format}, dataLen={Len}", width, height, pixelFormat, pixelData.Length); if (pixelData.Length == 0) { _logger.Warning("Hikvision OnFrameGrabbed: PixelData is empty"); return; } var args = new ImageGrabbedEventArgs(pixelData, width, height, pixelFormat); ImageGrabbed?.Invoke(this, args); } catch (Exception ex) { _logger.Error(ex, "Exception in Hikvision OnFrameGrabbed handler."); } } private void StopGrabbingInternal() { if (!_isGrabbing) return; try { _device?.StreamGrabber.StopGrabbing(); if (_device != null) _device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed; _isGrabbing = false; _logger.Information("Hikvision grabbing stopped."); } catch (Exception ex) when (ex is not CameraException) { _isGrabbing = false; _logger.Error(ex, "Error while stopping Hikvision grabbing."); throw new CameraException("Failed to stop grabbing.", ex); } } private void EnsureConnected() { if (!_isConnected) throw new InvalidOperationException("Hikvision camera is not connected. Call Open() first."); } /// /// 确保 SDK 全局初始化(只调用一次) /// private static void EnsureSdkInitialized() { if (_sdkInitialized) return; lock (_sdkInitLock) { if (_sdkInitialized) return; try { int ret = SDKSystem.Initialize(); if (ret != MvError.MV_OK) { _logger.Error("Hikvision SDK Initialize failed: 0x{ErrorCode:X8}", ret); throw new CameraException($"Hikvision SDK Initialize failed: 0x{ret:X8}"); } _sdkInitialized = true; _logger.Information("Hikvision SDK initialized successfully."); } catch (Exception ex) when (ex is not CameraException) { _logger.Error(ex, "Failed to initialize Hikvision SDK."); throw new CameraException("Failed to initialize Hikvision SDK.", ex); } } } }