using RobotNet10.RobotApp.Devices; using RobotNet10.RobotApp.SLAM; using RobotNet10.Shared.Geometry; using RobotNet10.Shared.Numbers; namespace RobotNet10.RobotApp.Detection; /// /// Detector for QR code markers using camera /// Polls QR camera for specific QR code and transforms pose to global frame /// /// /// Create a new QR code detection session /// /// QR code string to detect /// SLAM service for current robot pose /// QR camera device /// Camera pose relative to robot base public class QRDetector( string qrCode, ISLAMService slamService, ICameraQr camera, Pose cameraPose) : IDetector { private readonly Lock _lockPose = new(); private bool _isActive = false; private Thread? _pollingThread; private const int POLLING_FREQUENCY_HZ = 30; // Poll at 30Hz /// /// Detected marker pose in global frame /// Initialized with default pose, updated when QR code is detected /// public Pose MarkerPose { get { lock (_lockPose) { return field; } } private set { lock (_lockPose) { field = value; } } } /// /// Timestamp of the last successful QR detection /// public DateTime DetectionTime { get; private set; } = DateTime.MinValue; /// /// Activate the QR detection /// Starts polling QR camera in background thread /// public void Active() { if (_isActive) return; _isActive = true; // Create and start polling thread _pollingThread = new Thread(PollingThreadLoop) { Name = $"QRDetection_{qrCode}", IsBackground = true }; _pollingThread.Start(); } /// /// Disable the QR detection /// Stops polling thread /// public void Disable() { if (!_isActive) return; _isActive = false; // Wait for thread to finish _pollingThread?.Join(); _pollingThread = null; } /// /// Polling thread loop that checks QR camera periodically /// Runs at 30Hz to check for QR code detection /// private void PollingThreadLoop() { int delayMs = 1000 / POLLING_FREQUENCY_HZ; // ~33ms for 30Hz while (_isActive) { try { // Check if camera is connected if (!camera.IsConnected) { Thread.Sleep(delayMs); continue; } // Get QR code pose from camera (in camera frame) var poseStampedInCamera = camera[qrCode]; if (poseStampedInCamera.HasValue) { // Get current robot pose in global frame var currentRobotPose = slamService.CurrentPose; // Transform: camera frame -> robot frame -> global frame var poseInRobot = TransformPose(poseStampedInCamera.Value.Pose, cameraPose); var poseInGlobal = TransformPose(poseInRobot, currentRobotPose); // Update marker pose and detection time MarkerPose = poseInGlobal; DetectionTime = poseStampedInCamera.Value.Header.Stamp; // Console.WriteLine($"{DetectionTime:HH:mm:ss.ffffff} [QRDetector] Detected found QR code '{qrCode}' at global pose: [{MarkerPose.Position.X}, {MarkerPose.Position.Y}, {MarkerPose.Orientation.ToYawDegrees()}deg]"); } } catch { // Ignore errors in polling loop } // Sleep to maintain polling rate Thread.Sleep(delayMs); } } /// /// Transform a pose by another pose (pose composition) /// result = parentPose * childPose /// /// Pose in child frame /// Parent frame pose /// Transformed pose in parent's parent frame private static Pose TransformPose(Pose childPose, Pose parentPose) { // Get yaw angles double parentYaw = parentPose.Orientation.ToYawRadian(); double childYaw = childPose.Orientation.ToYawRadian(); // Rotate child position by parent orientation double cosParent = Math.Cos(parentYaw); double sinParent = Math.Sin(parentYaw); double globalX = parentPose.Position.X + childPose.Position.X * cosParent - childPose.Position.Y * sinParent; double globalY = parentPose.Position.Y + childPose.Position.X * sinParent + childPose.Position.Y * cosParent; // Combine orientations double globalYaw = NormalizeAngle(parentYaw + childYaw); return new Pose { Position = new Vector3 { X = globalX, Y = globalY, Z = parentPose.Position.Z + childPose.Position.Z }, Orientation = CreateQuaternionFromYaw(globalYaw) }; } /// /// Create a quaternion from yaw angle (rotation around Z axis) /// /// Yaw angle in radians /// Quaternion representing rotation around Z axis private static RobotNet10.Shared.Geometry.Quaternion CreateQuaternionFromYaw(double yaw) { double halfYaw = yaw / 2.0; return new RobotNet10.Shared.Geometry.Quaternion { X = 0, Y = 0, Z = Math.Sin(halfYaw), W = Math.Cos(halfYaw) }; } /// /// Normalize angle to [-π, π] /// /// Angle in radians /// Normalized angle in [-π, π] private static double NormalizeAngle(double angle) { while (angle > Math.PI) angle -= 2 * Math.PI; while (angle < -Math.PI) angle += 2 * Math.PI; return angle; } public void Dispose() { // Disable detector (stops thread) Disable(); // Suppress finalization since we've cleaned up GC.SuppressFinalize(this); } }