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
}