using RobotNet10.RobotApp.Client.Shared.Devices; using RobotNet10.RobotApp.Devices; using RobotNet10.Shared; using RobotNet10.Shared.Sensor; using Sick.SafetyScanners; using Sick.SafetyScanners.DataStructures; using Sick.SafetyScanners.Types; using System.Reactive; using UdpScanDataEventArgs = Sick.SafetyScanners.DataStructures.UdpScanDataEventArgs; namespace RobotNet10.RobotApp.Drivers.Sick; /// /// Cấu hình cho SICK Safety Scanner LiDAR Driver /// public class SickLidarDriverConfig { /// /// Địa chỉ IP của LiDAR (required) /// public string IpAddress { get; set; } = string.Empty; /// /// TCP port (COLA2) - mặc định: 2122 /// Chỉ dùng để config scanner nếu cần, không dùng để poll scan data /// public ushort TcpPort { get; set; } = 2122; /// /// UDP local port (for streaming mode) - mặc định: 0 (auto-assign) /// public ushort UdpLocalPort { get; set; } = 0; /// /// Host IP address (local machine IP for UDP streaming) - optional /// Nếu null hoặc empty, sẽ tự động detect IP cùng subnet với scanner IP khi connect /// public string? HostIp { get; set; } /// /// Start angle (degrees) - optional /// Nếu null, sẽ lấy từ scanner hoặc dùng all angles /// Lưu ý: Giá trị trong config là DEGREES, sẽ được convert sang RADIANS /// public double? StartAngle { get; set; } /// /// End angle (degrees) - optional /// Nếu null, sẽ lấy từ scanner hoặc dùng all angles /// Lưu ý: Giá trị trong config là DEGREES, sẽ được convert sang RADIANS /// public double? EndAngle { get; set; } /// /// Connect timeout (milliseconds) - mặc định: 5000 /// public int ConnectTimeoutMs { get; set; } = 5000; /// /// Command timeout (milliseconds) - mặc định: 5000 /// public int CommandTimeoutMs { get; set; } = 5000; /// /// Bật/tắt tự động reconnect - mặc định: true /// public bool AutoReconnectEnabled { get; set; } = true; /// /// Thời gian chờ trước khi reconnect (milliseconds) - mặc định: 3000 /// public int ReconnectDelayMs { get; set; } = 3000; /// /// Số lần reconnect tối đa - mặc định: 0 (unlimited) /// public int MaxReconnectAttempts { get; set; } = 0; /// /// Topic cho scan data (ROS-style), dùng trong LaserScan header hoặc publish - mặc định: /scan /// public string ScanTopic { get; set; } = "/scan"; /// /// Frame ID cho LaserScan header (coordinate frame) - mặc định: lidar_frame /// public string FrameId { get; set; } = "lidar_frame"; /// /// Offset góc (degrees) áp dụng lên mọi góc báo ra trong LaserScan. /// Ví dụ: lidar gửi -45°..225°, cần nhận -135°..135° thì đặt AngleOffsetDeg = -90. /// Mặc định: 0 (không offset). /// public double AngleOffsetDeg { get; set; } = 0; /// /// Khoảng cách đo tối thiểu (mét) - mặc định: 0.05m (5cm) /// SICK nanoScan3 hỗ trợ min range ~5cm. /// public double RangeMin { get; set; } = 0.05; /// /// Khoảng cách đo tối đa (mét) - mặc định: 40m /// SICK nanoScan3 hỗ trợ max range 40m (measurement data output). /// Giá trị này được dùng làm RangeMax trong LaserScan, ảnh hưởng tới /// SLAM, định vị XLOC, navigation và lọc điểm quét. /// public double RangeMax { get; set; } = 40.0; /// /// Parse cấu hình từ IConfigurationSection /// public static SickLidarDriverConfig Parse(IConfigurationSection configuration) { var config = new SickLidarDriverConfig(); // IpAddress (required) config.IpAddress = configuration["IpAddress"] ?? throw new InvalidOperationException( "IpAddress is required in connection configuration"); // TCP port - mặc định: 2122, hỗ trợ cả "TcpPort" và "Port" var tcpPortStr = configuration["TcpPort"] ?? configuration["Port"] ?? "2122"; if (!ushort.TryParse(tcpPortStr, out var tcpPort) || tcpPort == 0) { throw new InvalidOperationException($"Invalid TcpPort: {tcpPortStr}. Must be between 1 and 65535"); } config.TcpPort = tcpPort; // UDP local port - mặc định: 0 (auto-assign) var udpPortStr = configuration["UdpLocalPort"] ?? "0"; if (!ushort.TryParse(udpPortStr, out var udpPort)) { throw new InvalidOperationException($"Invalid UdpLocalPort: {udpPortStr}. Must be between 0 and 65535"); } config.UdpLocalPort = udpPort; // Host IP - optional, null = auto-detect config.HostIp = configuration["HostIp"]; // Start angle - optional (degrees) var startAngleStr = configuration["StartAngle"]; if (startAngleStr != null && double.TryParse(startAngleStr, out var startAngleDeg)) { config.StartAngle = startAngleDeg; } // End angle - optional (degrees) var endAngleStr = configuration["EndAngle"]; if (endAngleStr != null && double.TryParse(endAngleStr, out var endAngleDeg)) { config.EndAngle = endAngleDeg; } // Connect timeout - mặc định: 5000ms var connectTimeoutStr = configuration["ConnectTimeoutMs"] ?? "5000"; if (!int.TryParse(connectTimeoutStr, out var connectTimeout) || connectTimeout < 1000) { connectTimeout = 5000; // Default 5 seconds } config.ConnectTimeoutMs = connectTimeout; // Command timeout - mặc định: 5000ms var commandTimeoutStr = configuration["CommandTimeoutMs"] ?? "5000"; if (!int.TryParse(commandTimeoutStr, out var commandTimeout) || commandTimeout < 1000) { commandTimeout = 5000; // Default 5 seconds } config.CommandTimeoutMs = commandTimeout; // Auto reconnect settings var autoReconnectEnabled = configuration.GetValue("AutoReconnectEnabled"); config.AutoReconnectEnabled = autoReconnectEnabled ?? true; // Default true var reconnectDelayMs = configuration.GetValue("ReconnectDelayMs"); config.ReconnectDelayMs = reconnectDelayMs ?? 3000; // Default 3 seconds var maxReconnectAttempts = configuration.GetValue("MaxReconnectAttempts"); config.MaxReconnectAttempts = maxReconnectAttempts ?? 0; // Default 0 (unlimited) // ScanTopic - mặc định: /scan (hỗ trợ cả "ScanTopic" và "Topic") config.ScanTopic = configuration["ScanTopic"] ?? configuration["Topic"] ?? "/scan"; if (string.IsNullOrWhiteSpace(config.ScanTopic)) config.ScanTopic = "/scan"; // FrameId - mặc định: lidar_frame config.FrameId = configuration["FrameId"] ?? "lidar_frame"; if (string.IsNullOrWhiteSpace(config.FrameId)) config.FrameId = "lidar_frame"; // AngleOffsetDeg - mặc định: 0 (lidar -45°..225° → -135°..135° thì đặt -90) var angleOffsetStr = configuration["AngleOffsetDeg"]; if (angleOffsetStr != null && double.TryParse(angleOffsetStr, out var angleOffsetDeg)) { config.AngleOffsetDeg = angleOffsetDeg; } // RangeMin - mặc định: 0.05m (SICK nanoScan3 min range) var rangeMin = configuration.GetValue("RangeMin"); if (rangeMin.HasValue && rangeMin.Value >= 0) { config.RangeMin = rangeMin.Value; } // RangeMax - mặc định: 40m (SICK nanoScan3 max measurement range) var rangeMax = configuration.GetValue("RangeMax"); if (rangeMax.HasValue && rangeMax.Value > 0) { config.RangeMax = rangeMax.Value; } if (config.RangeMax <= config.RangeMin) { throw new InvalidOperationException( $"RangeMax must be greater than RangeMin. RangeMin={config.RangeMin}, RangeMax={config.RangeMax}"); } return config; } } /// /// Driver cho thiết bị LiDAR SICK Safety Scanner /// Tập trung vào cung cấp dữ liệu Measurement qua UDP streaming /// Kế thừa DeviceBase và implement ILidar /// [Device(DeviceType.Lidar, "SICK AG", "SickLidarDriver", "1.0.0", Description = "SICK Safety Scanner LiDAR Driver - UDP Streaming Mode")] public class SickLidarDriver : DeviceBase, ILidar { private readonly string _ipAddress; private readonly ushort _tcpPort; private readonly ushort _udpLocalPort; private readonly string? _hostIp; // Local IP address for UDP streaming (from config or auto-detected) private readonly double? _startAngle; // Start angle in radians (from config, optional) private readonly double? _endAngle; // End angle in radians (from config, optional) private readonly int _connectTimeoutMs; private readonly int _commandTimeoutMs; private readonly string _scanTopic; private readonly string _frameId; private readonly double _angleOffsetRad; private readonly double _rangeMinM; private readonly double _rangeMaxM; private SafetyScanner? _scanner; private readonly Lock _scannerLock = new(); // Cached device information (optional - only for display purposes) private string? _serialNumber; private string? _firmwareVersion; private string? _deviceName; private string? _orderNumber; private DateTime _lastUpdateTime = DateTime.UtcNow; private readonly Lock _infoLock = new(); // Cached scan data from UDP streaming private UdpScanData? _lastScanData; private DateTime? _lastScanDataTimestamp; private readonly Lock _scanDataLock = new(); /// /// Constructor /// public SickLidarDriver(string deviceId, string deviceName, IConfigurationSection configuration) : base(deviceId, deviceName, DeviceType.Lidar) { // Parse cấu hình từ IConfigurationSection var config = SickLidarDriverConfig.Parse(configuration); // Lưu các giá trị vào private fields _ipAddress = config.IpAddress; _tcpPort = config.TcpPort; _udpLocalPort = config.UdpLocalPort; _hostIp = string.IsNullOrWhiteSpace(config.HostIp) ? null : config.HostIp; // Convert StartAngle và EndAngle từ degrees sang radians if (config.StartAngle.HasValue) { _startAngle = (config.StartAngle.Value * Math.PI / 180.0); } else { _startAngle = null; } if (config.EndAngle.HasValue) { _endAngle = (config.EndAngle.Value * Math.PI / 180.0); } else { _endAngle = null; } _connectTimeoutMs = config.ConnectTimeoutMs; _commandTimeoutMs = config.CommandTimeoutMs; _scanTopic = config.ScanTopic; _frameId = config.FrameId; _angleOffsetRad = config.AngleOffsetDeg * Math.PI / 180.0; _rangeMinM = config.RangeMin; _rangeMaxM = config.RangeMax; // Set auto reconnect properties AutoReconnectEnabled = config.AutoReconnectEnabled; ReconnectDelayMs = config.ReconnectDelayMs; MaxReconnectAttempts = config.MaxReconnectAttempts; // Khởi tạo giá trị properties SetProperty("IpAddress", _ipAddress); SetProperty("TcpPort", _tcpPort.ToString()); SetProperty("UdpLocalPort", _udpLocalPort == 0 ? "Auto" : _udpLocalPort.ToString()); SetProperty("HostIp", _hostIp ?? "Auto-detect"); SetProperty("ConnectTimeoutMs", _connectTimeoutMs.ToString()); SetProperty("CommandTimeoutMs", _commandTimeoutMs.ToString()); // Hiển thị góc trong config (degrees) hoặc "Auto" SetProperty("StartAngle", _startAngle.HasValue ? $"{(_startAngle.Value * 180.0 / Math.PI):F1}°" : "Auto"); SetProperty("EndAngle", _endAngle.HasValue ? $"{(_endAngle.Value * 180.0 / Math.PI):F1}°" : "Auto"); SetProperty("ScanTopic", _scanTopic); SetProperty("FrameId", _frameId); SetProperty("AngleOffsetDeg", _angleOffsetRad == 0 ? "0" : $"{config.AngleOffsetDeg:F1}°"); SetProperty("RangeMin", _rangeMinM.ToString("F3")); SetProperty("RangeMax", _rangeMaxM.ToString("F3")); SetProperty("ConnectionStatus", "Disconnected"); SetProperty("SessionId", ""); SetProperty("SerialNumber", ""); SetProperty("FirmwareVersion", ""); SetProperty("DeviceName", ""); SetProperty("OrderNumber", ""); SetProperty("LastUpdateTime", ""); SetProperty("LastScanDataTimestamp", ""); } protected override IEnumerable CreatePropertyDescriptions() { yield return new PropertyDescription("IpAddress", "IP Address", "Địa chỉ IP của LiDAR") { DataType = "string", IsReadOnly = true, DisplayOrder = 1, Category = "Kết nối", DefaultValue = "" }; yield return new PropertyDescription("TcpPort", "TCP Port", "Port TCP của LiDAR (COLA2, chỉ dùng để config scanner)") { DataType = "number", IsReadOnly = true, DisplayOrder = 2, Category = "Kết nối", DefaultValue = "2122" }; yield return new PropertyDescription("UdpLocalPort", "UDP Local Port", "Port UDP local để nhận scan data (0 = auto-assign)") { DataType = "number", IsReadOnly = true, DisplayOrder = 3, Category = "Kết nối", DefaultValue = "0" }; yield return new PropertyDescription("HostIp", "Host IP", "Địa chỉ IP local để nhận UDP packets (auto-detect nếu không chỉ định)") { DataType = "string", IsReadOnly = true, DisplayOrder = 4, Category = "Kết nối", DefaultValue = "Auto-detect" }; yield return new PropertyDescription("ConnectTimeoutMs", "Connect Timeout (ms)", "Thời gian timeout khi kết nối (milliseconds)") { DataType = "number", IsReadOnly = true, DisplayOrder = 5, Category = "Cấu hình", DefaultValue = "5000" }; yield return new PropertyDescription("CommandTimeoutMs", "Command Timeout (ms)", "Thời gian timeout cho COLA2 commands (milliseconds)") { DataType = "number", IsReadOnly = true, DisplayOrder = 6, Category = "Cấu hình", DefaultValue = "5000" }; yield return new PropertyDescription("StartAngle", "Start Angle (degrees)", "Góc bắt đầu quét (độ). Nếu không chỉ định, sẽ lấy từ scanner hoặc dùng all angles. Giá trị trong config là degrees.") { DataType = "string", IsReadOnly = true, DisplayOrder = 7, Category = "Cấu hình", DefaultValue = "Auto" }; yield return new PropertyDescription("EndAngle", "End Angle (degrees)", "Góc kết thúc quét (độ). Nếu không chỉ định, sẽ lấy từ scanner hoặc dùng all angles. Giá trị trong config là degrees.") { DataType = "string", IsReadOnly = true, DisplayOrder = 8, Category = "Cấu hình", DefaultValue = "Auto" }; yield return new PropertyDescription("ScanTopic", "Scan Topic", "Topic cho scan data (ROS-style, mặc định /scan). Có thể dùng cho FrameId hoặc publish.") { DataType = "string", IsReadOnly = true, DisplayOrder = 9, Category = "Cấu hình", DefaultValue = "/scan" }; yield return new PropertyDescription("FrameId", "Frame ID", "Frame ID cho LaserScan header (coordinate frame).") { DataType = "string", IsReadOnly = true, DisplayOrder = 10, Category = "Cấu hình", DefaultValue = "lidar_frame" }; yield return new PropertyDescription("AngleOffsetDeg", "Angle Offset (deg)", "Offset góc (độ) áp dụng lên góc báo ra. Lidar -45°..225° → -135°..135° thì đặt -90.") { DataType = "string", IsReadOnly = true, DisplayOrder = 11, Category = "Cấu hình", DefaultValue = "0" }; yield return new PropertyDescription("RangeMin", "Range Min (m)", "Khoảng cách đo tối thiểu (mét). Mặc định 0.05m cho SICK nanoScan3.") { DataType = "number", IsReadOnly = true, DisplayOrder = 12, Category = "Cấu hình", DefaultValue = "0.05" }; yield return new PropertyDescription("RangeMax", "Range Max (m)", "Khoảng cách đo tối đa (mét). Mặc định 40m cho SICK nanoScan3 (measurement data output).") { DataType = "number", IsReadOnly = true, DisplayOrder = 13, Category = "Cấu hình", DefaultValue = "40.0" }; yield return new PropertyDescription("ConnectionStatus", "Connection Status", "Trạng thái kết nối") { DataType = "string", IsReadOnly = true, DisplayOrder = 11, Category = "Trạng thái", DefaultValue = "Disconnected" }; yield return new PropertyDescription("SessionId", "Session ID", "COLA2 Session ID hiện tại (nếu có)") { DataType = "string", IsReadOnly = true, DisplayOrder = 12, Category = "Trạng thái", DefaultValue = "" }; yield return new PropertyDescription("SerialNumber", "Serial Number", "Serial number của thiết bị") { DataType = "string", IsReadOnly = true, DisplayOrder = 8, Category = "Thông tin thiết bị", DefaultValue = "" }; yield return new PropertyDescription("FirmwareVersion", "Firmware Version", "Phiên bản firmware") { DataType = "string", IsReadOnly = true, DisplayOrder = 9, Category = "Thông tin thiết bị", DefaultValue = "" }; yield return new PropertyDescription("DeviceName", "Device Name", "Tên thiết bị") { DataType = "string", IsReadOnly = true, DisplayOrder = 10, Category = "Thông tin thiết bị", DefaultValue = "" }; yield return new PropertyDescription("OrderNumber", "Order Number", "Order number của thiết bị") { DataType = "string", IsReadOnly = true, DisplayOrder = 11, Category = "Thông tin thiết bị", DefaultValue = "" }; yield return new PropertyDescription("LastUpdateTime", "Last Update Time", "Thời gian cập nhật thông tin cuối cùng") { DataType = "string", IsReadOnly = true, DisplayOrder = 12, Category = "Trạng thái", DefaultValue = "" }; yield return new PropertyDescription("LastScanDataTimestamp", "Last Scan Data Timestamp", "Thời gian cập nhật dữ liệu scan cuối cùng") { DataType = "string", IsReadOnly = true, DisplayOrder = 13, Category = "Trạng thái", DefaultValue = "" }; yield return new PropertyDescription("NumberOfBeams", "Number Of Beams", "Số lượng dữ liệu trong scan") { DataType = "string", IsReadOnly = true, DisplayOrder = 13, Category = "Trạng thái", DefaultValue = "" }; } protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { lock (_scannerLock) { if (_scanner != null) { // Unsubscribe from events before disposing _scanner.ScanDataReceived -= OnSafetyScannerScanDataReceived; _scanner.Dispose(); _scanner = null; } // Tạo SafetyScanner instance với UDP port để enable UDP streaming // Driver này chỉ tập trung vào UDP streaming cho measurement data _scanner = new SafetyScanner(_ipAddress, _tcpPort, _udpLocalPort); } await Task.CompletedTask; } protected override async Task OnConnectAsync(CancellationToken cancellationToken) { SafetyScanner? scanner; lock (_scannerLock) { scanner = _scanner; } if (scanner == null) throw new InvalidOperationException("Scanner not initialized. Call InitializeAsync first."); // Subscribe to SafetyScanner events để nhận scan data từ UDP scanner.ScanDataReceived += OnSafetyScannerScanDataReceived; // Kết nối TCP trước (chưa mở COLA2 session) // UDP streaming không cần COLA2 session, nhưng ChangeCommSettings cần scanner.Connect(openCola2Session: false); // Lấy local UDP port (sau khi UDP streaming đã start) if (_udpLocalPort == 0) { throw new InvalidOperationException("Failed to get local UDP port after starting UDP streaming"); } // Xác định Host IP: từ config hoặc auto-detect string hostIp = _hostIp ?? DetectLocalIpAddress(_ipAddress); SetProperty("HostIp", hostIp); // Mở COLA2 session để có thể gọi ChangeCommSettings và RequestCurrentConfig scanner.Session.Open(); // Cập nhật session ID var sessionId = scanner.Session.SessionId; SetProperty("SessionId", sessionId?.ToString() ?? ""); // Xác định StartAngle và EndAngle: từ config, hoặc request từ scanner, hoặc default (all angles) double startAngle = 0.0; double endAngle = 0.0; if (_startAngle.HasValue && _endAngle.HasValue) { // Có trong config, sử dụng giá trị từ config startAngle = _startAngle.Value; endAngle = _endAngle.Value; } else { // Không có trong config, thử request từ scanner try { var configTimeoutDuration = TimeDuration.FromMilliseconds(_commandTimeoutMs); var currentConfig = scanner.RequestCurrentConfig(configTimeoutDuration); startAngle = currentConfig.StartAngle; endAngle = currentConfig.EndAngle; // Nếu cả hai đều 0, nghĩa là all angles (giữ nguyên) } catch (Exception ex) { // Nếu không request được, dùng default (0.0 = all angles) // Log error nhưng không throw - sẽ dùng all angles LastError = ex; OnErrorOccurred(ex, $"Failed to request current config from scanner, using default angles (all angles). Error: {ex.Message}"); startAngle = 0.0; endAngle = 0.0; } } // Cấu hình scanner để gửi UDP packets về đúng IP và port // Sử dụng ChangeCommSettingsAsync để đảm bảo lidar gửi dữ liệu về UDP server đúng var timeoutDuration = TimeDuration.FromMilliseconds(_commandTimeoutMs); var commSettings = CommSettings.Create( channel: 0, hostIp: hostIp, hostUdpPort: _udpLocalPort, generalSystemState: false, derivedSettings: true, measurementData: true, // Quan trọng: enable measurement data intrusionData: false, applicationData: false, publishingFrequency: 1, // Publish every scan startAngle: 0, // Từ config (radians), scanner (radians), hoặc default (all angles nếu = 0) endAngle: 0, interfaceType: InterfaceType.NonSafeEthernet, enabled: true ); scanner.ChangeCommSettings(commSettings, timeoutDuration); // Scanner đã được config để gửi UDP packets về {hostIp}:{localUdpPort} // Start UDP streaming TRƯỚC để lấy local UDP port // Sau đó sẽ dùng port này để config scanner scanner.StartUdpStreaming(); // Update properties với angles đã sử dụng (từ config, scanner, hoặc default) // Hiển thị góc ở dạng degrees cho dễ đọc if (startAngle == 0.0 && endAngle == 0.0) { SetProperty("StartAngle", "All angles (0°)"); SetProperty("EndAngle", "All angles (0°)"); } else { SetProperty("StartAngle", $"{startAngle * 180.0 / Math.PI:F1}°"); SetProperty("EndAngle", $"{endAngle * 180.0 / Math.PI:F1}°"); } SetProperty("ConnectionStatus", "Connected"); } /// /// Tự động detect local IP address cùng subnet với scanner IP /// private static string DetectLocalIpAddress(string scannerIpAddress) { try { // Parse scanner IP để lấy subnet if (!System.Net.IPAddress.TryParse(scannerIpAddress, out var scannerIp)) { throw new ArgumentException($"Invalid scanner IP address: {scannerIpAddress}", nameof(scannerIpAddress)); } var scannerBytes = scannerIp.GetAddressBytes(); if (scannerBytes.Length != 4) { throw new ArgumentException($"Only IPv4 is supported. Scanner IP: {scannerIpAddress}", nameof(scannerIpAddress)); } // Lấy tất cả network interfaces var networkInterfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); foreach (var networkInterface in networkInterfaces) { // Chỉ lấy interfaces đang up và không phải loopback if (networkInterface.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up || networkInterface.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) { continue; } // Lấy IP properties var ipProperties = networkInterface.GetIPProperties(); foreach (var ipAddress in ipProperties.UnicastAddresses) { // Chỉ lấy IPv4 addresses if (ipAddress.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue; var localIpBytes = ipAddress.Address.GetAddressBytes(); if (localIpBytes.Length != 4) continue; // Kiểm tra xem có cùng subnet không (giả định /24 subnet) // So sánh 3 bytes đầu (subnet mask 255.255.255.0) if (localIpBytes[0] == scannerBytes[0] && localIpBytes[1] == scannerBytes[1] && localIpBytes[2] == scannerBytes[2]) { return ipAddress.Address.ToString(); } } } // Nếu không tìm thấy IP cùng subnet, throw exception throw new InvalidOperationException( $"Could not detect local IP address in the same subnet as scanner IP {scannerIpAddress}. " + $"Please specify HostIp in connection configuration."); } catch (Exception ex) { throw new InvalidOperationException( $"Failed to auto-detect local IP address for scanner {scannerIpAddress}: {ex.Message}", ex); } } protected override async Task OnDisconnectAsync(CancellationToken cancellationToken) { SafetyScanner? scanner; lock (_scannerLock) { scanner = _scanner; } if (scanner != null) { try { // Stop UDP streaming trước scanner.StopUdpStreaming(); // Unsubscribe from SafetyScanner events scanner.ScanDataReceived -= OnSafetyScannerScanDataReceived; // Disconnect TCP (COLA2 session nếu có) scanner.Disconnect(); } catch (Exception ex) { // Log error nhưng không throw LastError = ex; OnErrorOccurred(ex, "Error during disconnect"); } SetProperty("ConnectionStatus", "Disconnected"); SetProperty("SessionId", ""); } await Task.CompletedTask; } /// /// Event handler for SafetyScanner.ScanDataReceived (UDP streaming) /// Converts SICK-specific UdpScanData to LaserScan /// private void OnSafetyScannerScanDataReceived(object? sender, UdpScanDataEventArgs e) { try { var timestamp = e.ScanData.Timestamp ?? DateTime.UtcNow; lock (_scanDataLock) { _lastScanData = e.ScanData; _lastScanDataTimestamp = timestamp; } // Convert to LaserScan and fire ILidar event if (e.ScanData.MeasurementData != null && !e.ScanData.MeasurementData.IsEmpty) { var laserScan = ConvertToLaserScan(e.ScanData, _frameId, _angleOffsetRad, _rangeMinM, _rangeMaxM); var eventArgs = new LidarScanDataEventArgs(timestamp, laserScan); ScanDataReceived?.Invoke(this, eventArgs); } // Update properties for UI SetProperty("LastScanDataTimestamp", _lastScanDataTimestamp?.ToString("yyyy-MM-dd HH:mm:ss.fff") ?? ""); if (e.ScanData.MeasurementData != null && !e.ScanData.MeasurementData.IsEmpty) { SetProperty("NumberOfBeams", e.ScanData.MeasurementData.NumberOfBeams.ToString()); } } catch (Exception ex) { LastError = ex; OnErrorOccurred(ex, "Error converting scan data"); } } protected override async Task OnResetAsync(CancellationToken cancellationToken) { // Clear cached device info lock (_infoLock) { _serialNumber = null; _firmwareVersion = null; _deviceName = null; _orderNumber = null; } SetProperty("SerialNumber", ""); SetProperty("FirmwareVersion", ""); SetProperty("DeviceName", ""); SetProperty("OrderNumber", ""); SetProperty("LastUpdateTime", ""); await Task.CompletedTask; } protected override async Task OnCheckConnectionAsync(CancellationToken cancellationToken) { SafetyScanner? scanner; lock (_scannerLock) { scanner = _scanner; } if (scanner == null) { OnErrorOccurred(new InvalidOperationException("Scanner is null"), "OnCheckConnectionAsync: Scanner is null"); return false; } try { // Check if UDP streaming is active // Driver này chỉ tập trung vào UDP streaming, nên chỉ cần check UDP streaming status if (scanner.IsUdpStreaming) { return true; // UDP streaming is active, connection is OK } else { OnErrorOccurred(new InvalidOperationException("UDP streaming is not active"), "OnCheckConnectionAsync: UDP streaming is not active"); return false; } } catch (Exception ex) { OnErrorOccurred(ex, $"OnCheckConnectionAsync: Unexpected error. " + $"Exception: {ex.GetType().Name}, Message: {ex.Message}"); return false; } } #region ILidar Implementation // ILidar Scan Data Properties Implementation (common for all Lidar brands) public LaserScan? CurrentMeasurementData { get { lock (_scanDataLock) { if (_lastScanData?.MeasurementData == null || _lastScanData.MeasurementData.IsEmpty) return null; return ConvertToLaserScan(_lastScanData, _frameId, _angleOffsetRad, _rangeMinM, _rangeMaxM); } } } public DateTime? LastScanDataTimestamp { get { lock (_scanDataLock) { return _lastScanDataTimestamp ?? _lastScanData?.Timestamp; } } } // ILidar Device Specifications public double MinAngleRad { get { lock (_scanDataLock) { if (_lastScanData?.DerivedValues != null && !_lastScanData.DerivedValues.IsEmpty) { return _lastScanData.DerivedValues.StartAngle + _angleOffsetRad; } return -Math.PI + _angleOffsetRad; } } } public double MaxAngleRad { get { lock (_scanDataLock) { if (_lastScanData?.DerivedValues != null && !_lastScanData.DerivedValues.IsEmpty) { var startAngle = _lastScanData.DerivedValues.StartAngle; var numberOfBeams = _lastScanData.DerivedValues.NumberOfBeams; var resolution = _lastScanData.DerivedValues.AngularBeamResolution; if (numberOfBeams > 0 && resolution != 0) { return startAngle + (numberOfBeams - 1) * resolution + _angleOffsetRad; } } return Math.PI + _angleOffsetRad; } } } public double MinRangeM => _rangeMinM; public double MaxRangeM => _rangeMaxM; public double? AngularResolutionRad { get { lock (_scanDataLock) { if (_lastScanData?.DerivedValues != null && !_lastScanData.DerivedValues.IsEmpty) { return _lastScanData.DerivedValues.AngularBeamResolution; } return null; } } } public double? ScanFrequencyHz { get { lock (_scanDataLock) { if (_lastScanData?.DerivedValues != null && !_lastScanData.DerivedValues.IsEmpty) { var scanTimeMs = _lastScanData.DerivedValues.ScanTime; if (scanTimeMs > 0) { return 1000.0 / scanTimeMs; // Convert ms to Hz } } return null; } } } public double FieldOfViewRad => Math.Abs(MaxAngleRad - MinAngleRad); public bool SupportsIntensity => true; // SICK Safety Scanner supports reflectivity/intensity public double? AccuracyM => null; // SICK Safety Scanner accuracy ~1cm // Events public event EventHandler? ScanDataReceived; #endregion // SICK-specific Properties (optional - for advanced usage) /// /// SICK-specific: Scan data gần nhất (parsed UDP packet) /// public UdpScanData? LastScanData { get { lock (_scanDataLock) { return _lastScanData; } } } /// /// SICK-specific: Derived values hiện tại (cấu hình scan: angles, resolution, number of beams) /// public DerivedValues? CurrentDerivedValues { get { lock (_scanDataLock) { return _lastScanData?.DerivedValues; } } } /// /// Converts SICK-specific MeasurementData to LaserScan structure /// Note: All angles in DerivedValues and ScanPoints are already in radians (converted by ParseDerivedValues and ParseMeasurementData) /// angleOffsetRad: offset (radians) applied to reported angles, e.g. -90° so that -45°..225° becomes -135°..135° /// private static LaserScan ConvertToLaserScan(UdpScanData scanData, string frameId, double angleOffsetRad, double configuredRangeMinM, double configuredRangeMaxM) { var sickMeasurementData = scanData.MeasurementData!; var sickDerivedValues = scanData.DerivedValues; // Convert SICK ScanPoints to ranges and intensities arrays var numberOfBeams = (int)sickMeasurementData.NumberOfBeams; var ranges = new double[numberOfBeams]; var intensities = new double[numberOfBeams]; for (int i = 0; i < numberOfBeams && i < sickMeasurementData.ScanPoints.Count; i++) { var sickPoint = sickMeasurementData.ScanPoints[i]; // Convert distance from mm to meters var distanceM = sickPoint.Distance / 1000.0; // If invalid, set to NaN ranges[i] = sickPoint.IsValid ? distanceM : double.NaN; // Convert reflectivity (0-255) to normalized intensity (0-1) intensities[i] = sickPoint.Reflectivity; } // Extract derived values for metadata // Note: StartAngle and AngularBeamResolution from DerivedValues are already in radians // (converted by ParseDerivedValues from sensor units: radians = (sensorUnits / 4194304.0) * 2π) double angleMin = 0.0; double angleMax = 0.0; double angleIncrement = 0.0; double scanTime = 0.0; // Range min/max lấy từ config (mặc định 0.05m và 40m cho SICK nanoScan3). // DerivedValues không cung cấp StartDistance/EndDistance nên phải cấu hình theo thiết bị. double rangeMin = configuredRangeMinM; double rangeMax = configuredRangeMaxM; if (sickDerivedValues != null && !sickDerivedValues.IsEmpty) { angleMin = sickDerivedValues.StartAngle + angleOffsetRad; // Already in radians, apply offset // Calculate end angle: start + (numberOfBeams - 1) * resolution if (sickDerivedValues.NumberOfBeams > 0) { angleIncrement = sickDerivedValues.AngularBeamResolution; // unchanged angleMax = angleMin + (sickDerivedValues.NumberOfBeams - 1) * angleIncrement; } // Calculate scan time from scan frequency (ms to seconds) if (sickDerivedValues.ScanTime > 0) { scanTime = sickDerivedValues.ScanTime / 1000.0; // Convert ms to seconds } } // Ensure timestamp is UTC and properly formatted var timestamp = scanData.Timestamp ?? DateTime.UtcNow; // If timestamp is from SICK (has Kind = Unspecified), ensure it's treated as UTC if (timestamp.Kind == DateTimeKind.Unspecified) { timestamp = DateTime.SpecifyKind(timestamp, DateTimeKind.Utc); } else if (timestamp.Kind == DateTimeKind.Local) { timestamp = timestamp.ToUniversalTime(); } return new LaserScan { Header = new Header { Stamp = timestamp, FrameId = frameId }, AngleMin = angleMin, AngleMax = angleMax, AngleIncrement = angleIncrement, TimeIncrement = 0.0, ScanTime = scanTime, RangeMin = rangeMin, RangeMax = rangeMax, Ranges = ranges, Intensities = intensities }; } // Optional: Device Information Methods (using SafetyScanner Request...Async methods) // These are optional because driver focuses on measurement data via UDP /// /// Gets the internal SafetyScanner instance (for advanced usage if needed) /// private SafetyScanner? GetScanner() { lock (_scannerLock) { return _scanner; } } /// /// Reads device information using SafetyScanner Request...Async methods /// Optional method - driver focuses on measurement data via UDP /// public async Task RefreshDeviceInfoAsync(CancellationToken cancellationToken = default) { var scanner = GetScanner(); if (scanner == null || !scanner.IsConnected) throw new InvalidOperationException("Scanner is not connected"); try { // Open COLA2 session if needed (for reading device info) bool needToOpenSession = !scanner.Session.IsOpen; if (needToOpenSession) { var timeoutDuration = TimeDuration.FromMilliseconds(_connectTimeoutMs); scanner.Connect(openCola2Session: true); } var timeout = TimeDuration.FromMilliseconds(_commandTimeoutMs); // Read device info using SafetyScanner Request... var serialNumber = scanner.RequestSerialNumber(timeout); var firmwareVersion = scanner.RequestFirmwareVersion(timeout); var deviceName = scanner.RequestDeviceName(timeout); var orderNumber = scanner.RequestOrderNumber(timeout); lock (_infoLock) { _serialNumber = serialNumber.Number; _firmwareVersion = firmwareVersion.Version; _deviceName = deviceName.Name; _orderNumber = orderNumber.Number; _lastUpdateTime = DateTime.UtcNow; } SetProperty("SerialNumber", _serialNumber); SetProperty("FirmwareVersion", _firmwareVersion); SetProperty("DeviceName", _deviceName); SetProperty("OrderNumber", _orderNumber); SetProperty("LastUpdateTime", _lastUpdateTime.ToString("yyyy-MM-dd HH:mm:ss")); } catch (Exception ex) { LastError = ex; OnErrorOccurred(ex, "Failed to refresh device info"); throw; } } public string? SerialNumber { get { lock (_infoLock) { return _serialNumber; } } } public string? FirmwareVersion { get { lock (_infoLock) { return _firmwareVersion; } } } public new string? DeviceName { get { lock (_infoLock) { return _deviceName; } } } public string? OrderNumber { get { lock (_infoLock) { return _orderNumber; } } } public DateTime LastUpdateTime { get { lock (_infoLock) { return _lastUpdateTime; } } } protected override void Dispose(bool disposing) { if (disposing) { // Unsubscribe from events and dispose scanner SafetyScanner? scanner; lock (_scannerLock) { scanner = _scanner; _scanner = null; } if (scanner != null) { scanner.ScanDataReceived -= OnSafetyScannerScanDataReceived; scanner.StopUdpStreaming(); scanner.Dispose(); } } base.Dispose(disposing); } }