using CartographerSharp.Sensor; using RobotNet10.RobotApp.SLAM.Cartographer; using RobotNet10.Shared.Geometry; using RobotNet10.Shared.Localization; namespace RobotNet10.RobotApp.SLAM.Cartographer.Helpers; /// /// Reason for MCL convergence /// public enum MclConvergenceReason { /// MCL has not converged yet NotConverged, /// Pose is stable and quality criteria met PoseStable, /// Timeout reached without convergence Timeout, /// Max iterations reached without convergence MaxIterations } /// /// Processes MCL (Monte Carlo Localization) convergence and pose tracking /// public class MclProcessor { private readonly ILogger _logger; private readonly CartographerConfiguration _config; private readonly Lock _lock = new(); private bool _running; private string? _primaryLidarId; private DateTime? _stableSinceUtc; private DateTime _runningSinceUtc = DateTime.MinValue; private int _iterationCount; private Pose? _lastPose; private Pose _initialPose; // Initial pose for fallback on timeout private OdometryData? _lastOdometryData; private double _lastOdometryTimestampSec; public MclProcessor(CartographerConfiguration config, ILogger logger) { _config = config ?? throw new ArgumentNullException(nameof(config)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public bool IsRunning { get { lock (_lock) { return _running; } } } public string? PrimaryLidarId { get { lock (_lock) { return _primaryLidarId; } } } /// /// Gets the initial pose used when MCL started (for fallback on timeout) /// public Pose InitialPose { get { lock (_lock) { return _initialPose; } } } /// /// Start MCL with initial seed pose /// public void Start(Pose seedPose, string effectivePrimaryLiderId, OccupancyGrid? grid) { lock (_lock) { _running = true; _primaryLidarId = effectivePrimaryLiderId; _iterationCount = 0; _lastPose = seedPose; _initialPose = seedPose; // Store initial pose for timeout fallback _stableSinceUtc = null; _runningSinceUtc = DateTime.UtcNow; _lastOdometryData = null; _lastOdometryTimestampSec = 0; } _logger.LogInformation("MclProcessor: MCL started with primary lidar: {LidarId}", effectivePrimaryLiderId); } /// /// Stop MCL /// public void Stop() { lock (_lock) { _running = false; _primaryLidarId = null; _stableSinceUtc = null; _iterationCount = 0; _lastPose = null; _lastOdometryData = null; _lastOdometryTimestampSec = 0; } } /// /// Update MCL seed pose during running state /// public void UpdateSeedPose(Pose newPose) { lock (_lock) { if (!_running) return; _iterationCount = 0; _lastPose = null; _stableSinceUtc = null; _lastOdometryData = null; _lastOdometryTimestampSec = 0; } _logger.LogInformation("MclProcessor: Seed pose updated"); } /// /// Check MCL convergence based on pose stability and quality criteria /// /// True if converged (for any reason), false otherwise public bool CheckConvergence(Pose currentPose, double reliability, double? mae, out int iterationCount) { return CheckConvergenceWithReason(currentPose, reliability, mae, out iterationCount) != MclConvergenceReason.NotConverged; } /// /// Check MCL convergence and return the reason /// /// Convergence reason indicating why MCL stopped or NotConverged if still running public MclConvergenceReason CheckConvergenceWithReason(Pose currentPose, double reliability, double? mae, out int iterationCount) { lock (_lock) { iterationCount = ++_iterationCount; if (_iterationCount < _config.Mcl.ConvergenceMinIterations) { _lastPose = currentPose; return MclConvergenceReason.NotConverged; } // Calculate pose change double poseDist = double.MaxValue; double yawDiff = double.MaxValue; if (_lastPose.HasValue) { double dx = currentPose.Position.X - _lastPose.Value.Position.X; double dy = currentPose.Position.Y - _lastPose.Value.Position.Y; poseDist = Math.Sqrt(dx * dx + dy * dy); double yawCur = GetYawFromPose(currentPose); double yawLast = GetYawFromPose(_lastPose.Value); double dyaw = yawCur - yawLast; while (dyaw > Math.PI) dyaw -= 2.0 * Math.PI; while (dyaw < -Math.PI) dyaw += 2.0 * Math.PI; yawDiff = Math.Abs(dyaw); } _lastPose = currentPose; // Check stability criteria bool poseStable = poseDist < _config.Mcl.ConvergencePoseChangeThresholdMeters && yawDiff < _config.Mcl.ConvergenceYawChangeThresholdRad; bool qualityGood = !_config.Mcl.EstimateReliability || (reliability >= _config.Mcl.ConvergenceReliabilityMin && (!mae.HasValue || mae.Value <= _config.Mcl.ConvergenceMaeMaxMeters)); var now = DateTime.UtcNow; double runningSec = (now - _runningSinceUtc).TotalSeconds; double stableDurationSec = _config.Mcl.ConvergenceStableDurationSeconds; double timeoutSec = _config.Mcl.ConvergenceTimeoutSeconds; if (qualityGood) { if (poseStable) { if (!_stableSinceUtc.HasValue) _stableSinceUtc = now; else if ((now - _stableSinceUtc.Value).TotalSeconds >= stableDurationSec) return MclConvergenceReason.PoseStable; } else { _stableSinceUtc = null; } } else { _stableSinceUtc = null; if (runningSec >= timeoutSec) return MclConvergenceReason.Timeout; } if (_iterationCount >= _config.Mcl.ConvergenceMaxIterations) return MclConvergenceReason.MaxIterations; return MclConvergenceReason.NotConverged; } } /// /// Process odometry data for MCL motion model /// public void ProcessOdometry(OdometryData odomData) { lock (_lock) { if (!_running || !_lastOdometryData.HasValue) { _lastOdometryData = odomData; _lastOdometryTimestampSec = odomData.Time / 10_000_000.0; return; } double currentTimeSec = odomData.Time / 10_000_000.0; double deltaTimeSec = currentTimeSec - _lastOdometryTimestampSec; if (deltaTimeSec > 0 && deltaTimeSec < 1.0) { var lastPose = _lastOdometryData.Value.Pose; var currentOdomPose = odomData.Pose; double dx = currentOdomPose.Translation.X - lastPose.Translation.X; double dy = currentOdomPose.Translation.Y - lastPose.Translation.Y; double linearX = dx / deltaTimeSec; double linearY = dy / deltaTimeSec; double yawLast = ((RobotNet10.Shared.Numbers.Quaternion)lastPose.Rotation).ToYawRadian(); double yawCurrent = ((RobotNet10.Shared.Numbers.Quaternion)currentOdomPose.Rotation).ToYawRadian(); double dyaw = yawCurrent - yawLast; while (dyaw > Math.PI) dyaw -= 2.0 * Math.PI; while (dyaw < -Math.PI) dyaw += 2.0 * Math.PI; double angularZ = dyaw / deltaTimeSec; // Store for use by MCL (would be passed to mcl.OnOdom in actual implementation) OnOdometryProcessed?.Invoke(deltaTimeSec, linearX, linearY, angularZ); } _lastOdometryData = odomData; _lastOdometryTimestampSec = currentTimeSec; } } // Event to signal odometry processing public event Action? OnOdometryProcessed; private static double GetYawFromPose(Pose p) { var q = p.Orientation; return Math.Atan2(2.0 * (q.W * q.Z + q.X * q.Y), 1.0 - 2.0 * (q.Y * q.Y + q.Z * q.Z)); } public void Dispose() { lock (_lock) { Stop(); } } }