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);
}
}