using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RobotNet10.RobotApp.Devices; using RobotNet10.Shared; using RobotNet10.Shared.Geometry; using RobotNet10.Shared.Sensor; using System; using System.Threading; using System.Threading.Tasks; namespace RobotNet10.RobotApp.MarkerDetection; /// /// Integration service to dispatch laser scan data to marker detection engine /// Provides rectangle or segment marker detection with pose estimation /// public class MarkerDetectionIntegrationService : IHostedService, IDisposable { private readonly MarkerDetectionIntegrationConfiguration _config; private readonly IDeviceProvider _deviceProvider; private readonly ILogger _logger; private readonly object _lock = new(); private readonly SemaphoreSlim _dispatchSemaphore = new SemaphoreSlim(1, 1); // Only 1 dispatch at a time private MarkerDetectionClient? _markerDetectionClient; // Timer for periodic marker pose updates and logging private Timer? _updateTimer; // Cached device references private ILidar? _lidarDevice; // Statistics (accessed from multiple threads) private long _laserScanCount = 0; private long _markerDetectionCount = 0; private DateTime _lastStatsLog = DateTime.UtcNow; private bool _disposed = false; private bool _isInitialized = false; public MarkerDetectionIntegrationService( IConfiguration configuration, IDeviceProvider deviceProvider, ILogger logger) { _deviceProvider = deviceProvider ?? throw new ArgumentNullException(nameof(deviceProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // Load configuration var configSection = configuration.GetSection("MarkerDetection:Integration"); if (!configSection.Exists()) { _logger.LogWarning("Configuration section 'MarkerDetection:Integration' not found. Using defaults."); _config = new MarkerDetectionIntegrationConfiguration(); } else { _config = new MarkerDetectionIntegrationConfiguration(); configSection.Bind(_config); } ValidateConfiguration(); } private void ValidateConfiguration() { if (_config.Enabled) { if (_config.LaserScanDispatchIntervalMs <= 0 || _config.LaserScanDispatchIntervalMs > 1000) { _logger.LogWarning("LaserScanDispatchIntervalMs {Interval} is out of range [1-1000]. Using default 50 ms.", _config.LaserScanDispatchIntervalMs); _config.LaserScanDispatchIntervalMs = 50; } if (_config.MarkerType == MarkerDetectionType.Rectangle && _config.RectangleMarkerOptions == null) { _logger.LogWarning("Rectangle marker type selected but options not provided. Using defaults."); _config.RectangleMarkerOptions = new MarkerDetectionRectangleOptions(); } if (_config.MarkerType == MarkerDetectionType.Segment && _config.SegmentMarkerOptions == null) { _logger.LogWarning("Segment marker type selected but options not provided. Using defaults."); _config.SegmentMarkerOptions = new MarkerDetectionSegmentOptions(); } } } public async Task StartAsync(CancellationToken cancellationToken) { if (!_config.Enabled) { _logger.LogInformation("MarkerDetectionIntegrationService is disabled in configuration"); return; } _logger.LogInformation("Starting MarkerDetectionIntegrationService..."); try { // Wait for devices to be connected _logger.LogInformation("Waiting for devices to be connected..."); var connected = await _deviceProvider.WaitForDevicesConnectedAsync(TimeSpan.FromMinutes(5), cancellationToken); if (!connected) { _logger.LogWarning("Timeout waiting for devices. MarkerDetectionIntegrationService will retry initialization."); _ = Task.Run(async () => await RetryInitializationAsync(cancellationToken), cancellationToken); return; } await InitializeAsync(cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error starting MarkerDetectionIntegrationService: {Message}", ex.Message); } } private async Task RetryInitializationAsync(CancellationToken cancellationToken) { const int maxRetries = 60; // 5 minutes with 5 second intervals int retryCount = 0; while (retryCount < maxRetries && !cancellationToken.IsCancellationRequested && !_isInitialized) { await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); var connected = await _deviceProvider.WaitForDevicesConnectedAsync(TimeSpan.FromSeconds(1), cancellationToken); if (connected) { _logger.LogInformation("Devices are now connected. Initializing MarkerDetectionIntegrationService..."); await InitializeAsync(cancellationToken); return; } retryCount++; if (retryCount % 12 == 0) // Log every minute { _logger.LogInformation("Still waiting for devices... (attempt {Attempt}/{MaxAttempts})", retryCount, maxRetries); } } if (retryCount >= maxRetries) { _logger.LogWarning("Timeout waiting for devices. MarkerDetectionIntegrationService will not be initialized."); } } private async Task InitializeAsync(CancellationToken cancellationToken) { lock (_lock) { if (_isInitialized) return; try { // Get LIDAR device reference if (!string.IsNullOrEmpty(_config.LidarDeviceId)) { var device = _deviceProvider.GetDevice(_config.LidarDeviceId); if (device != null) { _logger.LogInformation( "[LIDAR-INIT] Device '{DeviceId}' found. Type: {DeviceType}, IsConnected: {IsConnected}", _config.LidarDeviceId, device.GetType().Name, device.IsConnected); } if (device is ILidar lidar && device.IsConnected) { _lidarDevice = lidar; _logger.LogInformation( "[LIDAR-INIT] SUCCESS: LIDAR device '{DeviceId}' is initialized and connected. Ready to receive laser scan data.", _config.LidarDeviceId); } else { _logger.LogWarning("LIDAR device '{DeviceId}' not found or not connected. Marker detection will not work.", _config.LidarDeviceId); return; } } else { _logger.LogWarning("No LIDAR device ID configured. Marker detection will not work."); return; } // Create and initialize marker detection client _markerDetectionClient = new MarkerDetectionClient(_logger); // Initialize with appropriate marker type if (_config.MarkerType == MarkerDetectionType.Rectangle && _config.RectangleMarkerOptions != null) { _markerDetectionClient.InitializeRectangle(_config.RectangleMarkerOptions); } else if (_config.MarkerType == MarkerDetectionType.Segment && _config.SegmentMarkerOptions != null) { _markerDetectionClient.InitializeSegment(_config.SegmentMarkerOptions); } else { throw new InvalidOperationException("Invalid marker type configuration or missing marker options."); } _logger.LogInformation("MarkerDetectionClient initialized successfully"); // Start laser scan dispatch loop as background task (non-blocking) if (_lidarDevice != null) { _ = Task.Run(async () => await LaserScanDispatchLoopAsync(cancellationToken), cancellationToken); _logger.LogInformation("Laser scan dispatch loop started at {Rate} Hz", 1000.0 / _config.LaserScanDispatchIntervalMs); } // Start periodic update timer for pose logging and status updates _updateTimer = new Timer(UpdatePoseAndDiagnostics, null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); _logger.LogInformation("MarkerDetection update timer started (every 2 seconds)"); _isInitialized = true; _logger.LogInformation("MarkerDetectionIntegrationService initialized successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to initialize MarkerDetectionIntegrationService: {Message}", ex.Message); throw; } } } public async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping MarkerDetectionIntegrationService..."); try { lock (_lock) { _isInitialized = false; // Dispose timer _updateTimer?.Dispose(); _updateTimer = null; } // Wait a bit for dispatch loops to notice _isInitialized = false and stop _logger.LogInformation("Waiting for dispatch loops to stop..."); await Task.Delay(500, cancellationToken); // Clean up marker detection client if (_markerDetectionClient != null) { _markerDetectionClient.Dispose(); _markerDetectionClient = null; _logger.LogInformation("MarkerDetectionClient disposed successfully"); } LogFinalStatistics(); } catch (Exception ex) { _logger.LogError(ex, "Error stopping MarkerDetectionIntegrationService: {Message}", ex.Message); } await Task.CompletedTask; } /// /// Laser scan dispatch loop - runs in background task /// private async Task LaserScanDispatchLoopAsync(CancellationToken cancellationToken) { int noDataCounter = 0; while (_config.Enabled && !cancellationToken.IsCancellationRequested) { if (!_isInitialized || _markerDetectionClient == null || _lidarDevice == null || !_markerDetectionClient.IsInitialized) { await Task.Delay(100, cancellationToken); continue; } try { var scan = _lidarDevice.CurrentMeasurementData; // Log periodically to monitor intensities status (every ~5 seconds at 20Hz) if (scan.HasValue && _laserScanCount % 100 == 0) { var rawScan = scan.Value; if (rawScan.Intensities == null || rawScan.Intensities.Length == 0) { _logger.LogWarning( "[MARKER-DETECTION] LIDAR NOT providing intensities (Ranges: {RangeCount}). " + "Using default values. Check LIDAR RSSI configuration.", rawScan.Ranges?.Length ?? 0); } // else // { // _logger.LogInformation( // "[MARKER-DETECTION] ✓ LIDAR providing REAL intensities: {Count} values, " + // "Sample: [{I0:F3}, {I1:F3}, {I2:F3}]", // rawScan.Intensities.Length, // rawScan.Intensities[0], // rawScan.Intensities.Length > 1 ? rawScan.Intensities[1] : 0, // rawScan.Intensities.Length > 2 ? rawScan.Intensities[2] : 0); // } } if (scan == null) { noDataCounter++; if (noDataCounter % 100 == 0) // Log every 100 iterations (5 seconds at 20Hz) { // _logger.LogWarning( // "[LIDAR-DATA-DEBUG] CurrentMeasurementData is NULL. " + // "LIDAR device may not be connected or initialized. " + // "Configured device: '{LidarDeviceId}'. Missing data for {MissingIterations} iterations.", // _config.LidarDeviceId, noDataCounter); } continue; } var originalScan = scan.Value; // Assuming CurrentMeasurementData is nullable //in ra dữ liệu intensities và ranges để debug // _logger.LogInformation("LIDAR Data Debug - Ranges Count: {RangesCount}, Intensities Count: {IntensitiesCount}", // originalScan.Ranges?.Length ?? 0, originalScan.Intensities?.Length ?? 0); // Validate scan data before processing // LIDAR must provide at least range data (Intensities may be empty) if (originalScan.Ranges == null || originalScan.Ranges.Length == 0) { noDataCounter++; // if (noDataCounter % 100 == 0) // { // _logger.LogWarning( // "[LIDAR-DATA-DEBUG] No RANGE data in laser scan. " + // "Ranges: {RangeLength}, Intensities: {IntensityLength}. " + // "LIDAR device '{LidarDeviceId}' is not providing valid range measurements. " + // "Missing data for {MissingIterations} iterations.", // originalScan.Ranges?.Length ?? 0, // originalScan.Intensities?.Length ?? 0, // _config.LidarDeviceId, // noDataCounter); // } continue; } // Ensure Intensities array is not null // If LIDAR doesn't provide intensities, populate with default values // if (originalScan.Intensities == null || originalScan.Intensities.Length == 0) // { // originalScan.Intensities = new float[originalScan.Ranges.Length]; // for (int i = 0; i < originalScan.Intensities.Length; i++) // { // originalScan.Intensities[i] = 1.0f; // Default fallback value // } // } // Data is valid - reset counter noDataCounter = 0; // Log once per second when valid data is being received // if (noDataCounter == 0 && _laserScanCount % 20 == 0) // ~1 second at 20Hz // { // _logger.LogDebug( // "[LIDAR-DATA-OK] Valid laser scan received: Ranges: {RangeLength}, Intensities: {IntensityLength}", // originalScan.Ranges.Length, // originalScan.Intensities.Length); // } // Create updated scan with current timestamp var updatedScan = new LaserScan { Header = new Header { Seq = originalScan.Header.Seq, Stamp = DateTime.UtcNow, FrameId = "scan" }, AngleMin = (float)-2.356194496154785, AngleMax = (float)2.356194496154785, AngleIncrement =(float) 0.00581718236207962, TimeIncrement = (float) 6.172839493956417e-05, ScanTime = (float) 0.06666667014360428, RangeMin = (float) 0.0, RangeMax = (float) 100.0, Ranges = originalScan.Ranges, Intensities = originalScan.Intensities }; // In ra tất cả giũ liệu trong mảng Intensities // if (updatedScan.Intensities != null) // { // _logger.LogDebug("Intensities array length: {Length}", updatedScan.Intensities.Length); // for (int i = 0; i < Math.Min(811, updatedScan.Intensities.Length); i++) // { // _logger.LogInformation("Intensity[{Index}]: {Value}", i, originalScan.Intensities[i]); // _logger.LogInformation("Range[{Index}]: {Value}", i, updatedScan.Ranges[i]); // } // } // In ra tất cả giũ liệu trong mảng Ranges và Intensities trong cùng 1 dòng và Seq, Stamp, FrameId để debug // if (updatedScan.Intensities != null) // { // for (int i = 0; i < Math.Min(811, updatedScan.Intensities.Length); i++) // { // _logger.LogInformation("Detect marker Seq: {Seq}, Stamp: {Stamp}, FrameId: {FrameId}, Range[{Index}]: {Range}, Intensity[{Index}]: {Intensity}", // updatedScan.Header.Seq, // updatedScan.Header.Stamp.ToString("HH:mm:ss.ffff"), // updatedScan.Header.FrameId, // i, // updatedScan.Ranges[i], // i, // updatedScan.Intensities[i]); // } // } // Dispatch laser scan to marker detection // Re-check that client is still valid (could have been disposed by another thread) if (_markerDetectionClient == null || !_markerDetectionClient.IsInitialized) { continue; } await _dispatchSemaphore.WaitAsync(cancellationToken); try { // Double-check again after acquiring semaphore if (_markerDetectionClient != null && _markerDetectionClient.IsInitialized) { int rangeCount = updatedScan.Ranges?.Length ?? 0; int intensityCount = updatedScan.Intensities?.Length ?? 0; // _logger.LogInformation( // "[MARKER-DETECTION-DISPATCH] Sending to native code - " + // "Ranges: {RangeCount}, Intensities: {IntensityStatus} (Count: {IntensityCount})", // rangeCount, // updatedScan.Intensities == null ? "NULL" : "OK", // intensityCount); // if (updatedScan.Intensities != null && intensityCount > 2) // { // _logger.LogInformation( // "[MARKER-DETECTION-DISPATCH] First 3 intensities being sent: [{I0:F2}, {I1:F2}, {I2:F2}]", // updatedScan.Intensities[0], // updatedScan.Intensities[1], // updatedScan.Intensities[2]); // } _markerDetectionClient.DispatchLaserScan(updatedScan, _config.LaserScanSensorId ?? "scan"); Interlocked.Increment(ref _laserScanCount); } } finally { _dispatchSemaphore.Release(); } } catch (InvalidOperationException ex) when (ex.Message.Contains("not initialized")) { // MarkerDetectionClient was disposed during shutdown - this is normal, just exit gracefully _logger.LogDebug("Laser scan dispatch loop stopped: {Message}", ex.Message); break; } catch (Exception ex) { _logger.LogError(ex, "Error dispatching laser scan data: {Message}", ex.Message); } // Wait for next cycle await Task.Delay(_config.LaserScanDispatchIntervalMs, cancellationToken); } } /// /// Periodic timer callback to update marker pose and log diagnostics /// private void UpdatePoseAndDiagnostics(object? state) { if (!_isInitialized || _markerDetectionClient == null) return; try { // Get current marker pose if available if (_markerDetectionClient.IsInitialized) { var pose = _markerDetectionClient.GetMarkerPose(); if (pose != null) { Interlocked.Increment(ref _markerDetectionCount); // _logger.LogDebug("Marker pose: x={X}, y={Y}, z={Z}, qx={Qx}, qy={Qy}, qz={Qz}, qw={Qw}, frame={Frame}", // pose.Pose.Position[0], pose.Pose.Position[1], pose.Pose.Position[2], // pose.Pose.Orientation[0], pose.Pose.Orientation[1], pose.Pose.Orientation[2], pose.Pose.Orientation[3], // pose.Header.FrameId); } } } catch (Exception ex) { _logger.LogError(ex, "Error updating marker pose and diagnostics: {Message}", ex.Message); } // Log statistics periodically var now = DateTime.UtcNow; if ((now - _lastStatsLog).TotalSeconds >= 10) { _lastStatsLog = now; LogStatistics(); } } private void LogStatistics() { var laserCount = Interlocked.Read(ref _laserScanCount); var markerCount = Interlocked.Read(ref _markerDetectionCount); if (laserCount > 0) { var laserRate = laserCount / ((DateTime.UtcNow - _lastStatsLog).TotalSeconds + 0.001); // _logger.LogInformation( // "[MarkerDetection Stats] LaserScans: {LaserCount} (avg {LaserRate:F2} Hz), Marker Poses: {MarkerCount}", // laserCount, laserRate, markerCount); } else { // LIDAR data still empty - provide diagnostic info _logger.LogWarning( "[MarkerDetection Stats] No laser scan data received. " + "LIDAR Device ID configured: '{ConfigDeviceId}'. " + "Please verify LIDAR device is connected and providing data.", _config.LidarDeviceId); } } private void LogFinalStatistics() { var laserCount = Interlocked.Read(ref _laserScanCount); var markerCount = Interlocked.Read(ref _markerDetectionCount); _logger.LogInformation( "[MarkerDetection Final Stats] Total LaserScans: {LaserCount}, Total Marker Poses: {MarkerCount}", laserCount, markerCount); } /// /// Enable or disable marker detection /// public void SetEnableDetection(bool enable) { lock (_lock) { if (!_isInitialized || _markerDetectionClient == null || !_markerDetectionClient.IsInitialized) { _logger.LogWarning("Cannot set detection enable/disable: MarkerDetectionIntegrationService not initialized"); return; } try { _markerDetectionClient.SetEnableDetection(enable); _logger.LogInformation("Marker detection {Status}", enable ? "enabled" : "disabled"); } catch (Exception ex) { _logger.LogError(ex, "Error setting detection enable/disable: {Message}", ex.Message); } } } /// /// Get current marker pose /// public MarkerDetectionPoseStamped? GetMarkerPose() { lock (_lock) { if (!_isInitialized || _markerDetectionClient == null || !_markerDetectionClient.IsInitialized) { _logger.LogWarning("Cannot get marker pose: MarkerDetectionIntegrationService not initialized"); return null; } try { return _markerDetectionClient.GetMarkerPose(); } catch (Exception ex) { _logger.LogError(ex, "Error getting marker pose: {Message}", ex.Message); return null; } } } public void Dispose() { if (_disposed) return; _disposed = true; try { lock (_lock) { _isInitialized = false; } // Give loop time to stop Thread.Sleep(500); _updateTimer?.Dispose(); _markerDetectionClient?.Dispose(); _dispatchSemaphore?.Dispose(); } catch (Exception ex) { _logger.LogError(ex, "Error disposing MarkerDetectionIntegrationService: {Message}", ex.Message); } } } /// /// Marker detection type /// public enum MarkerDetectionType { Rectangle, Segment } /// /// Configuration for MarkerDetectionIntegrationService /// public class MarkerDetectionIntegrationConfiguration { /// /// Enable/disable the integration service /// public bool Enabled { get; set; } = false; /// /// Type of marker to detect (Rectangle or Segment) /// public MarkerDetectionType MarkerType { get; set; } = MarkerDetectionType.Rectangle; /// /// Rectangle marker detection options /// Required if MarkerType is Rectangle /// public MarkerDetectionRectangleOptions? RectangleMarkerOptions { get; set; } = null; /// /// Segment marker detection options /// Required if MarkerType is Segment /// public MarkerDetectionSegmentOptions? SegmentMarkerOptions { get; set; } = null; /// /// LIDAR device ID for laser scan dispatch /// public string LidarDeviceId { get; set; } = "scan_1"; /// /// Sensor ID for laser scan /// public string? LaserScanSensorId { get; set; } = "scan_1"; /// /// Frame ID for laser scan /// public string? LaserScanFrameId { get; set; } = "scan_1"; /// /// Laser scan data dispatch interval in milliseconds (default: 50ms = 20Hz) /// public int LaserScanDispatchIntervalMs { get; set; } = 50; /// /// Enable estimated marker pose for improved stability /// public bool EnableEstimateMarkerPose { get; set; } = false; /// /// Initial estimated marker pose X (meters) /// public double EstimateMarkerPoseX { get; set; } = 0.0; /// /// Initial estimated marker pose Y (meters) /// public double EstimateMarkerPoseY { get; set; } = 0.0; /// /// Initial estimated marker pose theta (radians) /// public double EstimateMarkerPoseTheta { get; set; } = 0.0; }