using RobotNet10.RobotApp.Client.Shared.Devices; using RobotNet10.RobotApp.Devices; using RobotNet10.Shared.Geometry; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; namespace RobotNet10.RobotApp.Drivers.Hik; /// /// Driver Hik cho Camera QR /// [Device(DeviceType.CameraQr, "Hik", "HikVisionQr", "1.0.0", Description = "Camera QR Code Detection Hik Driver")] public class HikVisionQr : DeviceBase, ICameraQr { private readonly ILogger _logger; private readonly Lock _dataLock = new(); private Thread? _receiveThread; private CancellationTokenSource? _receiveCts; private UdpClient? _udpClient; private IPEndPoint? _localEndPoint; private IPEndPoint? _remoteEndPoint; // Camera parameters private readonly int _cameraWidth; private readonly int _cameraHeight; private readonly double _distanceToQr; private readonly double _fovRangeWidthMm; private readonly double _fovRangeHeightMm; private readonly double _fovRangeDistanceMm; private readonly double _focalLengthPixels; private readonly string _localIp; private readonly int _localPort; private readonly double _qrDataTimeoutSeconds; // QR data dictionary (PoseStamped already contains timestamp in Header.Stamp) private readonly Dictionary _qrDictionary = []; // Data rate tracking private int _packetCount = 0; private double _currentDataRate = 0.0; public Dictionary Codes { get { lock (_dataLock) { var validCodes = new Dictionary(); var now = DateTime.UtcNow; var expiredKeys = new List(); foreach (var (code, poseStamped) in _qrDictionary) { var elapsed = (now - poseStamped.Header.Stamp).TotalSeconds; if (elapsed <= _qrDataTimeoutSeconds) { // Data is still valid validCodes[code] = poseStamped; } else { // Data expired, mark for removal expiredKeys.Add(code); } } // Clean up expired entries foreach (var key in expiredKeys) { _qrDictionary.Remove(key); } return validCodes; } } } public HikVisionQr(string deviceId, string deviceName, IConfigurationSection connection, IServiceProvider serviceProvider) : base(deviceId, deviceName, DeviceType.CameraQr) { _logger = serviceProvider.GetRequiredService().CreateLogger(); // Read configuration var autoReconnectEnabled = connection.GetValue("AutoReconnectEnabled"); var reconnectDelayMs = connection.GetValue("ReconnectDelayMs"); var maxReconnectAttempts = connection.GetValue("MaxReconnectAttempts"); // Camera parameters _cameraWidth = connection.GetValue("CameraWidth") ?? 1920; _cameraHeight = connection.GetValue("CameraHeight") ?? 1080; _distanceToQr = connection.GetValue("DistanceToQr") ?? 0.1; _fovRangeWidthMm = connection.GetValue("FovRangeWidthMm") ?? 170.0; _fovRangeHeightMm = connection.GetValue("FovRangeHeightMm") ?? 130.0; _fovRangeDistanceMm = connection.GetValue("FovRangeDistanceMm") ?? 100.0; _qrDataTimeoutSeconds = connection.GetValue("QrDataTimeoutSeconds") ?? 3.0; // Compute focal length in pixels from FOV range (pinhole camera model) // f = CameraWidth * FovRangeDistance / FovRangeWidth _focalLengthPixels = _cameraWidth * _fovRangeDistanceMm / _fovRangeWidthMm; // UDP parameters _localIp = connection.GetValue("LocalIP") ?? "192.168.254.100"; _localPort = connection.GetValue("LocalPort") ?? 1024; if (autoReconnectEnabled.HasValue) AutoReconnectEnabled = autoReconnectEnabled.Value; if (reconnectDelayMs.HasValue) ReconnectDelayMs = reconnectDelayMs.Value; if (maxReconnectAttempts.HasValue) MaxReconnectAttempts = maxReconnectAttempts.Value; // Initialize properties UpdateProperties(); } protected override IEnumerable CreatePropertyDescriptions() { yield return new PropertyDescription("DataRate", "Data Rate", "Tần số nhận dữ liệu (packets/s)") { DataType = "number", IsReadOnly = true, DisplayOrder = 1, Category = "Trạng thái", DefaultValue = "0" }; } protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { await Task.CompletedTask; } protected override async Task OnCheckConnectionAsync(CancellationToken cancellationToken) { return await Task.FromResult(_udpClient != null && _receiveThread != null && _receiveThread.IsAlive); } protected override async Task OnConnectAsync(CancellationToken cancellationToken) { // Create local endpoint and UDP client _localEndPoint = new IPEndPoint(IPAddress.Parse(_localIp), _localPort); _udpClient = new UdpClient(_localEndPoint); // Create cancellation token source for receive thread _receiveCts = new CancellationTokenSource(); // Create and start high-priority thread for receiving data _receiveThread = new Thread(() => ReceiveDataLoop(_receiveCts.Token)) { IsBackground = true, Priority = ThreadPriority.Highest, Name = $"HikQr-{DeviceId}-ReceiveThread" }; _receiveThread.Start(); await Task.CompletedTask; } protected override async Task OnDisconnectAsync(CancellationToken cancellationToken) { // Stop receive thread _receiveCts?.Cancel(); // Wait for thread to finish (with timeout) if (_receiveThread != null && _receiveThread.IsAlive) { var joined = _receiveThread.Join(TimeSpan.FromSeconds(5)); if (!joined) { // Thread didn't finish gracefully System.Diagnostics.Debug.WriteLine($"[HikQr] Receive thread did not finish in time"); } } _receiveCts?.Dispose(); _receiveCts = null; _receiveThread = null; // Close and dispose UDP client _udpClient?.Close(); _udpClient?.Dispose(); _udpClient = null; _localEndPoint = null; _remoteEndPoint = null; await Task.CompletedTask; } protected override async Task OnResetAsync(CancellationToken cancellationToken) { lock (_dataLock) { // Clear QR dictionary _qrDictionary.Clear(); _packetCount = 0; _currentDataRate = 0.0; UpdateProperties(); } await Task.CompletedTask; } private void ReceiveDataLoop(CancellationToken cancellationToken) { Thread.BeginThreadAffinity(); try { var stopwatch = Stopwatch.StartNew(); var packetCountInSecond = 0; while (!cancellationToken.IsCancellationRequested) { try { // Check if 1 second has elapsed if (stopwatch.Elapsed.TotalSeconds >= 1.0) { lock (_dataLock) { _currentDataRate = packetCountInSecond / stopwatch.Elapsed.TotalSeconds; UpdateProperties(); } // Reset counter and stopwatch packetCountInSecond = 0; stopwatch.Restart(); } // Check if UDP client is available if (_udpClient == null) break; // Set receive timeout to allow checking cancellation token _udpClient.Client.ReceiveTimeout = 100; // Receive UDP packet var receivedData = _udpClient.Receive(ref _remoteEndPoint); if (receivedData.Length > 0) { packetCountInSecond++; lock (_dataLock) { _packetCount++; } // Process received data UpdateData(receivedData); } } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) { // Timeout is expected, continue loop continue; } catch (OperationCanceledException) { break; } catch (Exception ex) { // Log error and continue _logger.LogError(ex, "[HikVisionQr] ReceiveDataLoop error"); } } stopwatch.Stop(); } finally { Thread.EndThreadAffinity(); } } private void UpdateData(byte[] receivedData) { lock (_dataLock) { if (receivedData.Length == 0) { // No data, clear dictionary _qrDictionary.Clear(); } else { try { // Extract message (40 bytes from position 37) if (receivedData.Length < 77) { // Invalid packet size return; } byte[] messageBytes = new byte[40]; Array.Copy(receivedData, 37, messageBytes, 0, 40); string message = Encoding.UTF8.GetString(messageBytes); // Extract QR data (9 bytes) byte[] dataBytes = new byte[9]; Array.Copy(messageBytes, 0, dataBytes, 0, 9); string qrCode = Encoding.UTF8.GetString(dataBytes); // Extract X position (3 bytes from position 11) byte[] xBytes = new byte[3]; Array.Copy(messageBytes, 11, xBytes, 0, 3); int qrPositionX = int.Parse(Encoding.ASCII.GetString(xBytes)); // Extract Y position (3 bytes from position 19) byte[] yBytes = new byte[3]; Array.Copy(messageBytes, 19, yBytes, 0, 3); int qrPositionY = int.Parse(Encoding.ASCII.GetString(yBytes)); // Extract angle (between ')' and '@') double qrAngle = 0; int start = message.IndexOf(')') + 1; int end = message.IndexOf('@', start); if (start > 0 && end > start) { string numberAngle = message[start..end]; qrAngle = double.Parse(numberAngle); } // Calculate pose immediately var pose = CreatePoseFromPixels( qrPositionX, qrPositionY, qrAngle, _cameraWidth, _cameraHeight, _distanceToQr, _focalLengthPixels); if (pose.HasValue) { // Console.WriteLine($"[HikQr] Detected QR Code: {qrCode}, X: {pose.Value.Pose.Position.X}, Y: {pose.Value.Pose.Position.Y}, Angle: {pose.Value.Pose.Orientation.ToYawDegrees()} degrees"); // Add or update dictionary (timestamp is in pose.Header.Stamp) _qrDictionary[qrCode] = pose.Value; } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[HikQr] UpdateData parsing error: {ex.Message}"); } } UpdateProperties(); } } private void UpdateProperties() { lock (_dataLock) { SetProperty("DataRate", _currentDataRate.ToString("F2")); } } protected override void Dispose(bool disposing) { if (disposing) { _receiveCts?.Cancel(); _receiveCts?.Dispose(); _udpClient?.Close(); _udpClient?.Dispose(); } base.Dispose(disposing); } #region ICameraQr Implementation public PoseStamped? this[string code] { get { lock (_dataLock) { if (!_qrDictionary.TryGetValue(code, out var poseStamped)) return null; // Check if data is still valid (within timeout) var elapsed = (DateTime.UtcNow - poseStamped.Header.Stamp).TotalSeconds; if (elapsed > _qrDataTimeoutSeconds) { // Data expired, remove from dictionary _qrDictionary.Remove(code); return null; } // Data is valid, return pose return poseStamped; } } } private static PoseStamped? CreatePoseFromPixels( int? px, int? py, double? angleDeg, int cameraWidth, int cameraHeight, double distanceMeters, double focalLengthPixels) { if (!px.HasValue || !py.HasValue || !angleDeg.HasValue) return null; // Convert pixel coordinates to meters using pinhole camera model // Same focal length for both axes (square pixels: 4.8μm × 4.8μm) double xMeters = (px.Value - cameraWidth / 2.0) * distanceMeters / focalLengthPixels; double yMeters = -(py.Value - cameraHeight / 2.0) * distanceMeters / focalLengthPixels; // Invert Y axis double zMeters = distanceMeters; // Convert angle to quaternion double angleRad = angleDeg.Value * Math.PI / 180.0; var quat = RobotNet10.Shared.Numbers.Quaternion.FromYawRadian(angleRad); // Create header var header = new RobotNet10.Shared.Header { Seq = 0, Stamp = DateTime.UtcNow, FrameId = "camera" }; // Create pose var pose = new Pose { Position = new Point { X = xMeters, Y = yMeters, Z = zMeters }, Orientation = new Quaternion(quat.X, quat.Y, quat.Z, quat.W) }; return new PoseStamped(header, pose); } #endregion }