2195 lines
83 KiB
C#
2195 lines
83 KiB
C#
using RobotNet10.RobotApp.Client.Shared.Devices;
|
|
using RobotNet10.RobotApp.Devices;
|
|
using RobotNet10.Shared;
|
|
using RobotNet10.Shared.Sensor;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using ColaB = Sick.Tim781s.Colab;
|
|
using ColaBCommands = Sick.Tim781s.Colab.Commands;
|
|
using ColaBComm = Sick.Tim781s.Colab.Communication;
|
|
using ColaBHelpers = Sick.Tim781s.Colab.Helpers;
|
|
|
|
namespace RobotNet10.RobotApp.Drivers.Sick;
|
|
|
|
/// <summary>
|
|
/// Cấu hình cho SICK Scan XD LiDAR Driver
|
|
/// </summary>
|
|
public class SickScanXdLidarDriverConfig
|
|
{
|
|
/// <summary>
|
|
/// Địa chỉ IP của LiDAR (required)
|
|
/// </summary>
|
|
public string IpAddress { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// TCP port - mặc định: 2112 (SICK scanner default port)
|
|
/// </summary>
|
|
public ushort TcpPort { get; set; } = 2112;
|
|
|
|
/// <summary>
|
|
/// Scanner type - ví dụ: "sick_tim_5xx", "sick_mrs_1xxx", "sick_nav_350", etc.
|
|
/// </summary>
|
|
public string ScannerType { get; set; } = "sick_tim_7xxS";
|
|
|
|
/// <summary>
|
|
/// Sử dụng binary protocol hay ASCII protocol - mặc định: true (binary)
|
|
/// Có thể là "ColaA", "ColaB", hoặc "Auto" (tự động phát hiện)
|
|
/// </summary>
|
|
public string Protocol { get; set; } = "Auto";
|
|
|
|
/// <summary>
|
|
/// Sử dụng binary protocol hay ASCII protocol - mặc định: true (binary)
|
|
/// Deprecated: Sử dụng Protocol thay thế
|
|
/// </summary>
|
|
[Obsolete("Use Protocol property instead")]
|
|
public bool UseBinaryProtocol { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Frame ID cho scan data - mặc định: "lidar_frame"
|
|
/// </summary>
|
|
public string FrameId { get; set; } = "scan";
|
|
|
|
/// <summary>
|
|
/// Connect timeout (milliseconds) - mặc định: 5000
|
|
/// </summary>
|
|
public int ConnectTimeoutMs { get; set; } = 5000;
|
|
|
|
/// <summary>
|
|
/// Command timeout (milliseconds) - mặc định: 5000
|
|
/// </summary>
|
|
public int CommandTimeoutMs { get; set; } = 5000;
|
|
|
|
/// <summary>
|
|
/// Read timeout (milliseconds) - mặc định: 5000
|
|
/// </summary>
|
|
public int ReadTimeoutMs { get; set; } = 5000;
|
|
|
|
/// <summary>
|
|
/// Bật/tắt tự động reconnect - mặc định: true
|
|
/// </summary>
|
|
public bool AutoReconnectEnabled { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Thời gian chờ trước khi reconnect (milliseconds) - mặc định: 3000
|
|
/// </summary>
|
|
public int ReconnectDelayMs { get; set; } = 3000;
|
|
|
|
/// <summary>
|
|
/// Số lần reconnect tối đa - mặc định: 0 (unlimited)
|
|
/// </summary>
|
|
public int MaxReconnectAttempts { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// Góc bắt đầu quét (degrees) - mặc định: -135.0° (tương đương -2.356 rad)
|
|
/// </summary>
|
|
public double StartAngle { get; set; } = -135.0;
|
|
|
|
/// <summary>
|
|
/// Góc kết thúc quét (degrees) - mặc định: 135.0° (tương đương 2.356 rad)
|
|
/// </summary>
|
|
public double EndAngle { get; set; } = 135.0;
|
|
|
|
/// <summary>
|
|
/// Parse cấu hình từ IConfigurationSection
|
|
/// </summary>
|
|
public static SickScanXdLidarDriverConfig Parse(IConfigurationSection connection)
|
|
{
|
|
var config = new SickScanXdLidarDriverConfig();
|
|
|
|
// IpAddress (required)
|
|
config.IpAddress = connection["IpAddress"] ?? throw new InvalidOperationException(
|
|
"IpAddress is required in connection configuration");
|
|
|
|
// TCP port - mặc định: 2112
|
|
var tcpPortStr = connection["TcpPort"] ?? connection["Port"] ?? "2112";
|
|
if (!ushort.TryParse(tcpPortStr, out var tcpPort) || tcpPort == 0)
|
|
{
|
|
throw new InvalidOperationException($"Invalid TcpPort: {tcpPortStr}. Must be between 1 and 65535");
|
|
}
|
|
config.TcpPort = tcpPort;
|
|
|
|
// Scanner type - mặc định: "sick_tim_5xx"
|
|
config.ScannerType = connection["ScannerType"] ?? "sick_tim_5xx";
|
|
|
|
// Protocol selection - mặc định: "Auto"
|
|
var protocolStr = connection["Protocol"] ?? connection["UseBinaryProtocol"] ?? "Auto";
|
|
protocolStr = protocolStr.Trim();
|
|
|
|
if (string.Equals(protocolStr, "ColaA", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(protocolStr, "Cola-A", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(protocolStr, "ASCII", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
config.Protocol = "ColaA";
|
|
config.UseBinaryProtocol = false;
|
|
}
|
|
else if (string.Equals(protocolStr, "ColaB", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(protocolStr, "Cola-B", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(protocolStr, "Binary", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
config.Protocol = "ColaB";
|
|
config.UseBinaryProtocol = true;
|
|
}
|
|
else if (string.Equals(protocolStr, "Auto", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
config.Protocol = "Auto";
|
|
// Default to ColaB for Auto mode
|
|
config.UseBinaryProtocol = true;
|
|
}
|
|
else
|
|
{
|
|
// Fallback: try to parse as boolean for backward compatibility
|
|
if (bool.TryParse(protocolStr, out var useBinary))
|
|
{
|
|
config.UseBinaryProtocol = useBinary;
|
|
config.Protocol = useBinary ? "ColaB" : "ColaA";
|
|
}
|
|
else
|
|
{
|
|
config.Protocol = "Auto";
|
|
config.UseBinaryProtocol = true;
|
|
}
|
|
}
|
|
|
|
// Frame ID - mặc định: "lidar_frame"
|
|
config.FrameId = connection["FrameId"] ?? "lidar_frame";
|
|
|
|
// Connect timeout - mặc định: 5000ms
|
|
var connectTimeoutStr = connection["ConnectTimeoutMs"] ?? "5000";
|
|
if (!int.TryParse(connectTimeoutStr, out var connectTimeout) || connectTimeout < 1000)
|
|
{
|
|
connectTimeout = 5000;
|
|
}
|
|
config.ConnectTimeoutMs = connectTimeout;
|
|
|
|
// Command timeout - mặc định: 5000ms
|
|
var commandTimeoutStr = connection["CommandTimeoutMs"] ?? "5000";
|
|
if (!int.TryParse(commandTimeoutStr, out var commandTimeout) || commandTimeout < 1000)
|
|
{
|
|
commandTimeout = 5000;
|
|
}
|
|
config.CommandTimeoutMs = commandTimeout;
|
|
|
|
// Read timeout - mặc định: 5000ms
|
|
var readTimeoutStr = connection["ReadTimeoutMs"] ?? "5000";
|
|
if (!int.TryParse(readTimeoutStr, out var readTimeout) || readTimeout < 1000)
|
|
{
|
|
readTimeout = 5000;
|
|
}
|
|
config.ReadTimeoutMs = readTimeout;
|
|
|
|
// Auto reconnect settings
|
|
var autoReconnectEnabled = connection.GetValue<bool?>("AutoReconnectEnabled");
|
|
config.AutoReconnectEnabled = autoReconnectEnabled ?? true;
|
|
|
|
var reconnectDelayMs = connection.GetValue<int?>("ReconnectDelayMs");
|
|
config.ReconnectDelayMs = reconnectDelayMs ?? 3000;
|
|
|
|
var maxReconnectAttempts = connection.GetValue<int?>("MaxReconnectAttempts");
|
|
config.MaxReconnectAttempts = maxReconnectAttempts ?? 0;
|
|
|
|
// StartAngle và EndAngle (degrees) - mặc định: -135° đến 135°
|
|
var startAngle = connection.GetValue<double?>("StartAngle");
|
|
config.StartAngle = startAngle ?? -135.0;
|
|
|
|
var endAngle = connection.GetValue<double?>("EndAngle");
|
|
config.EndAngle = endAngle ?? 135.0;
|
|
|
|
return config;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Driver cho thiết bị LiDAR SICK Scan XD
|
|
/// Sử dụng TCP/IP và SOPAS protocol để giao tiếp với SICK scanners
|
|
/// Kế thừa DeviceBase và implement ILidar
|
|
/// </summary>
|
|
[Device(DeviceType.Lidar, "SICK AG", "SickScanXdLidarDriver", "1.0.0",
|
|
Description = "SICK Scan XD LiDAR Driver - TCP/IP SOPAS Protocol")]
|
|
public class SickScanXdLidarDriver : DeviceBase, ILidar
|
|
{
|
|
private readonly string _ipAddress;
|
|
private readonly ushort _tcpPort;
|
|
private readonly string _scannerType;
|
|
private readonly bool _useBinaryProtocol; // Config preference
|
|
private readonly string _protocolPreference; // "ColaA", "ColaB", or "Auto"
|
|
private readonly string _frameId;
|
|
private readonly int _connectTimeoutMs;
|
|
private readonly int _commandTimeoutMs;
|
|
private readonly int _readTimeoutMs;
|
|
|
|
// Actual protocol being used (may differ from config if auto-detection switches)
|
|
private bool _actualUseBinaryProtocol;
|
|
private readonly Lock _protocolLock = new();
|
|
|
|
private System.Net.Sockets.TcpClient? _tcpClient;
|
|
private NetworkStream? _networkStream;
|
|
private readonly Lock _connectionLock = new();
|
|
|
|
// ColaB session for binary protocol
|
|
private ColaB.ColaBSession? _colaBSession;
|
|
private ColaBComm.TcpClient? _colaBTcpClient;
|
|
|
|
// Scan data
|
|
private LaserScan? _lastScanData;
|
|
private DateTime? _lastScanDataTimestamp;
|
|
private readonly Lock _scanDataLock = new();
|
|
|
|
// Device specifications (cached from scanner)
|
|
private double _minAngleRad = -Math.PI;
|
|
private double _maxAngleRad = Math.PI;
|
|
private double _minRangeM = 0.05;
|
|
private double _maxRangeM = 25.0;
|
|
private double? _angularResolutionRad;
|
|
private double? _scanFrequencyHz;
|
|
private bool _supportsIntensity = true;
|
|
|
|
// Background task for receiving scan data
|
|
private CancellationTokenSource? _receiveCts;
|
|
private Task? _receiveTask;
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public SickScanXdLidarDriver(string deviceId, string deviceName, IConfigurationSection connection)
|
|
: base(deviceId, deviceName, DeviceType.Lidar)
|
|
{
|
|
// Parse cấu hình từ IConfigurationSection
|
|
var config = SickScanXdLidarDriverConfig.Parse(connection);
|
|
|
|
// Lưu các giá trị vào private fields
|
|
_ipAddress = config.IpAddress;
|
|
_tcpPort = config.TcpPort;
|
|
_scannerType = config.ScannerType;
|
|
_useBinaryProtocol = config.UseBinaryProtocol;
|
|
_protocolPreference = config.Protocol;
|
|
_frameId = config.FrameId;
|
|
_connectTimeoutMs = config.ConnectTimeoutMs;
|
|
_commandTimeoutMs = config.CommandTimeoutMs;
|
|
_readTimeoutMs = config.ReadTimeoutMs;
|
|
|
|
// Set auto reconnect properties
|
|
AutoReconnectEnabled = config.AutoReconnectEnabled;
|
|
ReconnectDelayMs = config.ReconnectDelayMs;
|
|
MaxReconnectAttempts = config.MaxReconnectAttempts;
|
|
|
|
// Set scan angle range from config (convert from degrees to radians)
|
|
_minAngleRad = config.StartAngle * Math.PI / 180.0;
|
|
_maxAngleRad = config.EndAngle * Math.PI / 180.0;
|
|
|
|
// Initialize actual protocol to config preference (will be auto-detected during connection)
|
|
_actualUseBinaryProtocol = _useBinaryProtocol;
|
|
|
|
// Khởi tạo giá trị properties
|
|
SetProperty("IpAddress", _ipAddress);
|
|
SetProperty("TcpPort", _tcpPort.ToString());
|
|
SetProperty("ScannerType", _scannerType);
|
|
SetProperty("Protocol", _protocolPreference);
|
|
SetProperty("UseBinaryProtocol", _useBinaryProtocol.ToString());
|
|
SetProperty("ActualProtocol", _useBinaryProtocol ? "ColaB" : "ColaA");
|
|
SetProperty("FrameId", _frameId);
|
|
SetProperty("StartAngle", config.StartAngle.ToString("F1"));
|
|
SetProperty("EndAngle", config.EndAngle.ToString("F1"));
|
|
SetProperty("ConnectionStatus", "Disconnected");
|
|
SetProperty("LastScanDataTimestamp", "");
|
|
}
|
|
|
|
protected override IEnumerable<PropertyDescription> 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")
|
|
{
|
|
DataType = "number",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 2,
|
|
Category = "Kết nối",
|
|
DefaultValue = "2112"
|
|
};
|
|
|
|
yield return new PropertyDescription("ScannerType", "Scanner Type", "Loại scanner SICK")
|
|
{
|
|
DataType = "string",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 3,
|
|
Category = "Cấu hình",
|
|
DefaultValue = "sick_tim_5xx"
|
|
};
|
|
|
|
yield return new PropertyDescription("Protocol", "Protocol", "Protocol sử dụng: ColaA, ColaB, hoặc Auto (tự động phát hiện)")
|
|
{
|
|
DataType = "string",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 4,
|
|
Category = "Cấu hình",
|
|
DefaultValue = "Auto"
|
|
};
|
|
|
|
yield return new PropertyDescription("UseBinaryProtocol", "Use Binary Protocol", "Sử dụng binary protocol hay ASCII (cấu hình - deprecated)")
|
|
{
|
|
DataType = "boolean",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 5,
|
|
Category = "Cấu hình",
|
|
DefaultValue = "true"
|
|
};
|
|
|
|
yield return new PropertyDescription("ActualProtocol", "Actual Protocol", "Protocol thực tế đang sử dụng (ColaA hoặc ColaB)")
|
|
{
|
|
DataType = "string",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 6,
|
|
Category = "Trạng thái",
|
|
DefaultValue = "ColaB"
|
|
};
|
|
|
|
yield return new PropertyDescription("FrameId", "Frame ID", "Frame ID cho scan data")
|
|
{
|
|
DataType = "string",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 5,
|
|
Category = "Cấu hình",
|
|
DefaultValue = "lidar_frame"
|
|
};
|
|
|
|
yield return new PropertyDescription("StartAngle", "Start Angle", "Góc bắt đầu quét (degrees)")
|
|
{
|
|
DataType = "number",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 7,
|
|
Category = "Cấu hình",
|
|
DefaultValue = "-135.0"
|
|
};
|
|
|
|
yield return new PropertyDescription("EndAngle", "End Angle", "Góc kết thúc quét (degrees)")
|
|
{
|
|
DataType = "number",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 8,
|
|
Category = "Cấu hình",
|
|
DefaultValue = "135.0"
|
|
};
|
|
|
|
yield return new PropertyDescription("ConnectionStatus", "Connection Status", "Trạng thái kết nối")
|
|
{
|
|
DataType = "string",
|
|
IsReadOnly = true,
|
|
DisplayOrder = 9,
|
|
Category = "Trạng thái",
|
|
DefaultValue = "Disconnected"
|
|
};
|
|
|
|
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 = 10,
|
|
Category = "Trạng thái",
|
|
DefaultValue = ""
|
|
};
|
|
}
|
|
|
|
protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Không cần khởi tạo gì đặc biệt ở đây
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
protected override async Task OnConnectAsync(CancellationToken cancellationToken)
|
|
{
|
|
lock (_connectionLock)
|
|
{
|
|
if (_tcpClient != null && _tcpClient.Connected)
|
|
{
|
|
return; // Already connected
|
|
}
|
|
|
|
// Disconnect existing connection if any
|
|
DisconnectInternal();
|
|
}
|
|
|
|
// Try to connect and auto-detect protocol
|
|
bool connected = false;
|
|
Exception? lastException = null;
|
|
|
|
// Determine which protocol to try first
|
|
bool tryColaBFirst = _protocolPreference == "Auto" ? _useBinaryProtocol :
|
|
(_protocolPreference == "ColaB");
|
|
|
|
// First, try with configured protocol preference
|
|
try
|
|
{
|
|
if (tryColaBFirst)
|
|
{
|
|
await ConnectColaBAsync(cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
await ConnectAsciiAsync(cancellationToken);
|
|
}
|
|
|
|
// Try to detect which protocol the scanner actually supports
|
|
var detectedProtocol = await DetectProtocolAsync(cancellationToken);
|
|
if (detectedProtocol.HasValue)
|
|
{
|
|
lock (_protocolLock)
|
|
{
|
|
_actualUseBinaryProtocol = detectedProtocol.Value;
|
|
}
|
|
SetProperty("ActualProtocol", _actualUseBinaryProtocol ? "ColaB" : "ColaA");
|
|
|
|
// If detected protocol differs from what we connected with, reconnect
|
|
if (_actualUseBinaryProtocol != _useBinaryProtocol)
|
|
{
|
|
DisconnectInternal();
|
|
|
|
if (_actualUseBinaryProtocol)
|
|
{
|
|
await ConnectColaBAsync(cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
await ConnectAsciiAsync(cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
connected = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastException = ex;
|
|
}
|
|
|
|
// If connection failed and protocol is Auto, try the other protocol as fallback
|
|
if (!connected && _protocolPreference == "Auto")
|
|
{
|
|
try
|
|
{
|
|
DisconnectInternal();
|
|
|
|
if (!tryColaBFirst)
|
|
{
|
|
await ConnectColaBAsync(cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
await ConnectAsciiAsync(cancellationToken);
|
|
}
|
|
|
|
// Try to detect protocol again
|
|
var detectedProtocol = await DetectProtocolAsync(cancellationToken);
|
|
if (detectedProtocol.HasValue)
|
|
{
|
|
lock (_protocolLock)
|
|
{
|
|
_actualUseBinaryProtocol = detectedProtocol.Value;
|
|
}
|
|
SetProperty("ActualProtocol", _actualUseBinaryProtocol ? "ColaB" : "ColaA");
|
|
}
|
|
else
|
|
{
|
|
// Use the protocol we connected with
|
|
lock (_protocolLock)
|
|
{
|
|
_actualUseBinaryProtocol = !_useBinaryProtocol;
|
|
}
|
|
SetProperty("ActualProtocol", _actualUseBinaryProtocol ? "ColaB" : "ColaA");
|
|
}
|
|
|
|
connected = true;
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
throw new InvalidOperationException($"Failed to connect to SICK scanner at {_ipAddress}:{_tcpPort} with both ColaA and ColaB protocols. Last error: {ex2.Message}", ex2);
|
|
}
|
|
}
|
|
|
|
// Initialize scanner (send SOPAS commands)
|
|
// Don't fail if initialization has issues - some scanners may work without full initialization
|
|
try
|
|
{
|
|
await InitializeScannerAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log error but continue - scanner may still work
|
|
OnErrorOccurred(ex, "Scanner initialization had issues but continuing");
|
|
}
|
|
|
|
// Start receiving scan data in background
|
|
StartReceiveTask();
|
|
|
|
SetProperty("ConnectionStatus", "Connected");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connect using ColaB (binary) protocol
|
|
/// </summary>
|
|
private async Task ConnectColaBAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
_colaBTcpClient = new ColaBComm.TcpClient(_ipAddress, _tcpPort);
|
|
_colaBSession = new ColaB.ColaBSession(_colaBTcpClient);
|
|
|
|
// Open session (connects to scanner)
|
|
_colaBSession.Open(_connectTimeoutMs);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_colaBSession?.Dispose();
|
|
_colaBTcpClient?.Dispose();
|
|
_colaBSession = null;
|
|
_colaBTcpClient = null;
|
|
throw new InvalidOperationException($"Failed to connect to SICK scanner at {_ipAddress}:{_tcpPort} using ColaB: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connect using ASCII protocol
|
|
/// </summary>
|
|
private async Task ConnectAsciiAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Create new TCP connection
|
|
var tcpClient = new System.Net.Sockets.TcpClient();
|
|
try
|
|
{
|
|
// Connect with timeout
|
|
var connectTask = tcpClient.ConnectAsync(_ipAddress, _tcpPort);
|
|
var timeoutTask = Task.Delay(_connectTimeoutMs, cancellationToken);
|
|
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
|
|
|
|
if (completedTask == timeoutTask)
|
|
{
|
|
tcpClient.Close();
|
|
throw new TimeoutException($"Connection timeout after {_connectTimeoutMs}ms");
|
|
}
|
|
|
|
await connectTask; // Ensure connection completed successfully
|
|
|
|
_networkStream = tcpClient.GetStream();
|
|
_networkStream.ReadTimeout = _readTimeoutMs;
|
|
_networkStream.WriteTimeout = _commandTimeoutMs;
|
|
|
|
lock (_connectionLock)
|
|
{
|
|
_tcpClient = tcpClient;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
tcpClient?.Close();
|
|
throw new InvalidOperationException($"Failed to connect to SICK scanner at {_ipAddress}:{_tcpPort}: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
private void DisconnectInternal()
|
|
{
|
|
// Stop receive task
|
|
_receiveCts?.Cancel();
|
|
_receiveTask?.Wait(1000); // Wait max 1 second
|
|
_receiveCts?.Dispose();
|
|
_receiveCts = null;
|
|
_receiveTask = null;
|
|
|
|
// Close ColaB session if using binary protocol
|
|
lock (_protocolLock)
|
|
{
|
|
if (_actualUseBinaryProtocol)
|
|
{
|
|
_colaBSession?.Dispose();
|
|
_colaBTcpClient?.Dispose();
|
|
_colaBSession = null;
|
|
_colaBTcpClient = null;
|
|
}
|
|
else
|
|
{
|
|
// Close network stream
|
|
_networkStream?.Close();
|
|
_networkStream?.Dispose();
|
|
_networkStream = null;
|
|
|
|
// Close TCP client
|
|
_tcpClient?.Close();
|
|
_tcpClient?.Dispose();
|
|
_tcpClient = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detect which protocol (ColaA or ColaB) the scanner supports
|
|
/// Returns true for ColaB, false for ColaA, null if detection failed
|
|
/// </summary>
|
|
private async Task<bool?> DetectProtocolAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Try to send a simple command (sRN DeviceState) with current connection
|
|
// If we're connected via ColaB, try ColaB first
|
|
if (_colaBSession != null && _colaBSession.IsOpen)
|
|
{
|
|
try
|
|
{
|
|
var readCmd = new ColaBCommands.ReadCommand("DeviceState");
|
|
_colaBSession.ExecuteCommand(readCmd, _commandTimeoutMs);
|
|
if (readCmd.WasSuccessful)
|
|
{
|
|
return true; // ColaB works
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
// If we're connected via ASCII, try ASCII
|
|
if (_tcpClient != null && _tcpClient.Connected && _networkStream != null)
|
|
{
|
|
try
|
|
{
|
|
var reply = await SendSopasCommandAsciiAsync("sRN DeviceState", cancellationToken);
|
|
if (!string.IsNullOrEmpty(reply) && !reply.Contains("sFA"))
|
|
{
|
|
return false; // ColaA works
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
return null; // Detection failed
|
|
}
|
|
|
|
protected override async Task OnDisconnectAsync(CancellationToken cancellationToken)
|
|
{
|
|
lock (_connectionLock)
|
|
{
|
|
DisconnectInternal();
|
|
}
|
|
|
|
SetProperty("ConnectionStatus", "Disconnected");
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
protected override async Task OnResetAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Clear cached scan data
|
|
lock (_scanDataLock)
|
|
{
|
|
_lastScanData = null;
|
|
_lastScanDataTimestamp = null;
|
|
}
|
|
|
|
SetProperty("LastScanDataTimestamp", "");
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
protected override async Task<bool> OnCheckConnectionAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Read protocol outside of lock to avoid await in lock
|
|
bool useBinary;
|
|
lock (_protocolLock)
|
|
{
|
|
useBinary = _actualUseBinaryProtocol;
|
|
}
|
|
|
|
if (useBinary)
|
|
{
|
|
// Check ColaB session - just verify TCP connection is still open
|
|
// Don't send command here as it may fail if scanner is still initializing
|
|
// The actual connection verification happens during OnConnectAsync
|
|
try
|
|
{
|
|
if (_colaBSession == null || !_colaBSession.IsOpen)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Session is open, connection is valid
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lock (_connectionLock)
|
|
{
|
|
if (_tcpClient == null || !_tcpClient.Connected)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Try to send a simple SOPAS command to check connection
|
|
try
|
|
{
|
|
var reply = await SendSopasCommandAsync("sRN DeviceIdent", cancellationToken);
|
|
return !string.IsNullOrEmpty(reply);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#region SOPAS Protocol
|
|
|
|
/// <summary>
|
|
/// Initialize scanner by sending SOPAS commands
|
|
/// </summary>
|
|
private async Task InitializeScannerAsync(CancellationToken cancellationToken)
|
|
{
|
|
bool useBinary;
|
|
lock (_protocolLock)
|
|
{
|
|
useBinary = _actualUseBinaryProtocol;
|
|
}
|
|
|
|
if (useBinary)
|
|
{
|
|
// Use ColaB protocol for binary communication
|
|
await InitializeScannerColaBAsync(cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
// Use ASCII protocol
|
|
await InitializeScannerAsciiAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize scanner using ColaB (binary) protocol
|
|
/// </summary>
|
|
private async Task InitializeScannerColaBAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (_colaBSession == null)
|
|
throw new InvalidOperationException("ColaB session not initialized");
|
|
|
|
// Set access mode first (authentication) - required for most SICK scanners
|
|
// Level 3 with password hash "F4724744" (default for most scanners)
|
|
// For TiM7xxS safety scanner, use "6FD62C05"
|
|
try
|
|
{
|
|
string passwordHash = "F4724744"; // Default password hash
|
|
if (_scannerType.Contains("7xxS", StringComparison.OrdinalIgnoreCase) ||
|
|
_scannerType.Contains("safety", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
passwordHash = "6FD62C05"; // Safety scanner password hash
|
|
}
|
|
|
|
var methodCmd = new ColaBCommands.MethodCommand("SetAccessMode", "3", passwordHash);
|
|
_colaBSession.ExecuteCommand(methodCmd, _commandTimeoutMs);
|
|
if (!methodCmd.WasSuccessful)
|
|
{
|
|
OnErrorOccurred(new InvalidOperationException("Failed to set access mode"),
|
|
"Failed to authenticate with scanner via ColaB");
|
|
// Continue anyway - some scanners may not require authentication
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnErrorOccurred(ex, "Error setting access mode via ColaB (may not be required)");
|
|
// Continue anyway - some scanners may not require authentication
|
|
}
|
|
|
|
// Request device identification
|
|
try
|
|
{
|
|
var readCmd = new ColaBCommands.ReadCommand("DeviceIdent");
|
|
_colaBSession.ExecuteCommand(readCmd, _commandTimeoutMs);
|
|
if (readCmd.WasSuccessful && !string.IsNullOrEmpty(readCmd.ReplyValue))
|
|
{
|
|
// Parse device info if needed
|
|
}
|
|
else
|
|
{
|
|
// Don't throw - some scanners may not respond to DeviceIdent immediately
|
|
OnErrorOccurred(new InvalidOperationException("DeviceIdent command failed"),
|
|
"Failed to read device identification via ColaB (may be normal)");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Don't throw - scanner may still work without DeviceIdent
|
|
OnErrorOccurred(ex, "Failed to read device identification via ColaB (may be normal)");
|
|
}
|
|
|
|
// Start scan data transmission
|
|
// For TiM781S, we try to enable scan data event
|
|
// Note: Scanner may already be in measurement mode, so we skip LMCstartmeas if it fails
|
|
try
|
|
{
|
|
// Check scanner state
|
|
try
|
|
{
|
|
await SendSopasCommandAsync("sRN SCdevicestate", cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
// Check scan data configuration
|
|
try
|
|
{
|
|
var currentConfig = await SendSopasCommandAsync("sRN LMDscandatacfg", cancellationToken);
|
|
Console.WriteLine($"[SICK-RSSI-CONFIG] Current LMDscandatacfg: {currentConfig}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[SICK-RSSI-CONFIG] Failed to read LMDscandatacfg: {ex.Message}");
|
|
}
|
|
|
|
// Configure scan data output to include RSSI/remission values
|
|
// Format: sWN LMDscandatacfg <output_channel> <remission> <resolution> <unit> <encoder>
|
|
// Parameters:
|
|
// output_channel: 01 (standard output channel)
|
|
// remission: 01 (enable RSSI/remission data) - THIS IS KEY FOR INTENSITIES!
|
|
// resolution: 01 (8-bit resolution)
|
|
// unit: 00 (distance unit)
|
|
// encoder: 00 (no encoder)
|
|
try
|
|
{
|
|
Console.WriteLine("[SICK-RSSI-CONFIG] Configuring LMDscandatacfg to enable RSSI/remission output...");
|
|
var configResult = await SendSopasCommandAsync("sWN LMDscandatacfg 01 01 01 00 00", cancellationToken);
|
|
Console.WriteLine($"[SICK-RSSI-CONFIG] LMDscandatacfg set successfully: {configResult}");
|
|
_supportsIntensity = true;
|
|
|
|
// Verify configuration was applied
|
|
await Task.Delay(100, cancellationToken); // Give scanner time to apply settings
|
|
var verifyConfig = await SendSopasCommandAsync("sRN LMDscandatacfg", cancellationToken);
|
|
Console.WriteLine($"[SICK-RSSI-CONFIG] Verified LMDscandatacfg: {verifyConfig}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[SICK-RSSI-CONFIG] ERROR: Failed to set LMDscandatacfg: {ex.Message}");
|
|
OnErrorOccurred(ex, "Failed to enable RSSI output in LIDAR configuration");
|
|
_supportsIntensity = false;
|
|
}
|
|
|
|
// Try to start measurement (may fail if already started - that's OK)
|
|
try
|
|
{
|
|
await SendSopasCommandAsync("sMN LMCstartmeas", cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
// Continue - scanner may already be in measurement mode
|
|
}
|
|
|
|
// Apply settings (Run) - this is usually safe
|
|
try
|
|
{
|
|
await SendSopasCommandAsync("sMN Run", cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
// Continue anyway
|
|
}
|
|
|
|
// Try to enable scan data event
|
|
// Note: Some scanners may send scan data automatically without this command
|
|
try
|
|
{
|
|
await SendSopasCommandAsync("sEN LMDscandata 1", cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
// Continue anyway - scanner may send scan data automatically
|
|
}
|
|
|
|
// Try to read scan data directly (polling mode) as fallback
|
|
try
|
|
{
|
|
// Use ColaB session directly to get raw binary data
|
|
if (_colaBSession != null)
|
|
{
|
|
var readCmd = new ColaBCommands.ReadCommand("LMDscandata");
|
|
_colaBSession.ExecuteCommand(readCmd, _commandTimeoutMs);
|
|
|
|
if (readCmd.WasSuccessful && readCmd.RawReplyData != null)
|
|
{
|
|
// Try to parse binary format
|
|
if (TryParseScanDataFromColaBBinary(readCmd.RawReplyData, out var parsedScanBinary))
|
|
{
|
|
UpdateScanData(parsedScanBinary);
|
|
}
|
|
else
|
|
{
|
|
// Fallback to ASCII parser
|
|
var replyString = System.Text.Encoding.ASCII.GetString(readCmd.RawReplyData);
|
|
var parsedScan = ParseLmdScandata(replyString);
|
|
if (parsedScan.HasValue && parsedScan.Value.Ranges != null)
|
|
{
|
|
UpdateScanData(parsedScan.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Continue anyway - scanner may still send scan data
|
|
OnErrorOccurred(ex, "Some initialization commands failed, but continuing (scanner may send scan data automatically)");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize scanner using ASCII protocol
|
|
/// </summary>
|
|
private async Task InitializeScannerAsciiAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Request device identification
|
|
var deviceIdent = await SendSopasCommandAsync("sRN DeviceIdent", cancellationToken);
|
|
if (!string.IsNullOrEmpty(deviceIdent))
|
|
{
|
|
// Parse device info if needed
|
|
}
|
|
|
|
// Start scan data transmission
|
|
// Command depends on scanner type, but "sEN LMDscandata 1" is common
|
|
try
|
|
{
|
|
await SendSopasCommandAsync("sEN LMDscandata 1", cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
// Some scanners may not support this command, continue anyway
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send SOPAS command and wait for reply
|
|
/// </summary>
|
|
private async Task<string> SendSopasCommandAsync(string command, CancellationToken cancellationToken)
|
|
{
|
|
bool useBinary;
|
|
lock (_protocolLock)
|
|
{
|
|
useBinary = _actualUseBinaryProtocol;
|
|
}
|
|
|
|
if (useBinary)
|
|
{
|
|
// Use ColaB protocol
|
|
return await SendSopasCommandColaBAsync(command, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
// Use ASCII protocol
|
|
return await SendSopasCommandAsciiAsync(command, cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send SOPAS command using ColaB (binary) protocol
|
|
/// </summary>
|
|
private async Task<string> SendSopasCommandColaBAsync(string command, CancellationToken cancellationToken)
|
|
{
|
|
if (_colaBSession == null)
|
|
throw new InvalidOperationException("ColaB session not initialized");
|
|
|
|
try
|
|
{
|
|
// Determine command type and create appropriate command
|
|
ColaBCommands.IColaBCommand colaBCommand;
|
|
if (command.StartsWith("sRN ", StringComparison.Ordinal))
|
|
{
|
|
// Read command
|
|
var variableName = command.Substring(4).Trim();
|
|
colaBCommand = new ColaBCommands.ReadCommand(variableName);
|
|
}
|
|
else if (command.StartsWith("sWN ", StringComparison.Ordinal))
|
|
{
|
|
// Write command
|
|
var parts = command.Substring(4).Split(' ', 2);
|
|
if (parts.Length == 2)
|
|
{
|
|
colaBCommand = new ColaBCommands.WriteCommand(parts[0], parts[1]);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"Invalid write command format: {command}");
|
|
}
|
|
}
|
|
else if (command.StartsWith("sMN ", StringComparison.Ordinal))
|
|
{
|
|
// Method command
|
|
var parts = command.Substring(4).Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length > 0)
|
|
{
|
|
var methodName = parts[0];
|
|
var parameters = parts.Skip(1).ToArray();
|
|
colaBCommand = new ColaBCommands.MethodCommand(methodName, parameters);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"Invalid method command format: {command}");
|
|
}
|
|
}
|
|
else if (command.StartsWith("sEN ", StringComparison.Ordinal))
|
|
{
|
|
// Event command (Set Event Name) - similar to write but for events
|
|
// Format: "sEN <eventName> <value>"
|
|
var parts = command.Substring(4).Split(' ', 2);
|
|
if (parts.Length == 2)
|
|
{
|
|
// Use WriteCommand for sEN as it's similar in structure
|
|
colaBCommand = new ColaBCommands.WriteCommand(parts[0], parts[1]);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"Invalid event command format: {command}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"Unsupported command format: {command}");
|
|
}
|
|
|
|
// Execute command
|
|
_colaBSession.ExecuteCommand(colaBCommand, _commandTimeoutMs);
|
|
|
|
if (!colaBCommand.WasSuccessful)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
// Get reply value
|
|
if (colaBCommand is ColaBCommands.ReadCommand readCmd)
|
|
{
|
|
var reply = readCmd.ReplyValue ?? string.Empty;
|
|
return reply;
|
|
}
|
|
else if (colaBCommand is ColaBCommands.MethodCommand methodCmd)
|
|
{
|
|
var reply = methodCmd.ReplyValue ?? string.Empty;
|
|
return reply;
|
|
}
|
|
else if (colaBCommand is ColaBCommands.WriteCommand writeCmd)
|
|
{
|
|
// WriteCommand doesn't have ReplyValue, but WasSuccessful indicates success
|
|
return "OK"; // Return OK to indicate success
|
|
}
|
|
else
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"Failed to send ColaB command: {command}", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send SOPAS command using ASCII protocol
|
|
/// </summary>
|
|
private async Task<string> SendSopasCommandAsciiAsync(string command, CancellationToken cancellationToken)
|
|
{
|
|
NetworkStream? stream;
|
|
lock (_connectionLock)
|
|
{
|
|
stream = _networkStream;
|
|
if (stream == null || _tcpClient == null || !_tcpClient.Connected)
|
|
{
|
|
throw new InvalidOperationException("Not connected to scanner");
|
|
}
|
|
}
|
|
|
|
// Format SOPAS command (ASCII mode)
|
|
// STX + command + ETX
|
|
var commandBytes = Encoding.ASCII.GetBytes(command);
|
|
var message = new List<byte> { 0x02 }; // STX
|
|
message.AddRange(commandBytes);
|
|
message.Add(0x03); // ETX
|
|
|
|
// Send command
|
|
await stream.WriteAsync(message.ToArray(), 0, message.Count, cancellationToken);
|
|
await stream.FlushAsync(cancellationToken);
|
|
|
|
// Read reply
|
|
var buffer = new byte[4096];
|
|
var totalBytes = 0;
|
|
var startTime = DateTime.UtcNow;
|
|
|
|
while (DateTime.UtcNow - startTime < TimeSpan.FromMilliseconds(_commandTimeoutMs))
|
|
{
|
|
if (stream.DataAvailable)
|
|
{
|
|
var bytesRead = await stream.ReadAsync(buffer, totalBytes, buffer.Length - totalBytes, cancellationToken);
|
|
if (bytesRead == 0)
|
|
break;
|
|
|
|
totalBytes += bytesRead;
|
|
|
|
// Check if we have complete message (ends with ETX)
|
|
if (totalBytes > 0 && buffer[totalBytes - 1] == 0x03)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await Task.Delay(10, cancellationToken);
|
|
}
|
|
}
|
|
|
|
if (totalBytes == 0)
|
|
{
|
|
throw new TimeoutException("No reply from scanner");
|
|
}
|
|
|
|
// Parse reply (skip STX, remove ETX)
|
|
var replyStart = 0;
|
|
if (buffer[0] == 0x02) replyStart = 1; // Skip STX
|
|
|
|
var replyEnd = totalBytes;
|
|
if (buffer[totalBytes - 1] == 0x03) replyEnd = totalBytes - 1; // Remove ETX
|
|
|
|
var replyLength = replyEnd - replyStart;
|
|
if (replyLength <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return Encoding.ASCII.GetString(buffer, replyStart, replyLength);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scan Data Reception
|
|
|
|
/// <summary>
|
|
/// Start background task to receive scan data
|
|
/// </summary>
|
|
private void StartReceiveTask()
|
|
{
|
|
_receiveCts?.Cancel();
|
|
_receiveCts?.Dispose();
|
|
|
|
_receiveCts = new CancellationTokenSource();
|
|
_receiveTask = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await ReceiveScanDataLoopAsync(_receiveCts.Token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnErrorOccurred(ex, "ReceiveScanDataLoopAsync exception");
|
|
}
|
|
}, _receiveCts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Main loop to receive and parse scan data
|
|
/// </summary>
|
|
private async Task ReceiveScanDataLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
bool useBinary;
|
|
lock (_protocolLock)
|
|
{
|
|
useBinary = _actualUseBinaryProtocol;
|
|
}
|
|
|
|
if (useBinary)
|
|
{
|
|
await ReceiveScanDataLoopColaBAsync(cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
await ReceiveScanDataLoopAsciiAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Receive scan data loop for ASCII protocol
|
|
/// </summary>
|
|
private async Task ReceiveScanDataLoopAsciiAsync(CancellationToken cancellationToken)
|
|
{
|
|
var buffer = new byte[480000]; // Large buffer for scan data
|
|
var partialBuffer = new List<byte>();
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
NetworkStream? stream;
|
|
bool shouldDelay = false;
|
|
lock (_connectionLock)
|
|
{
|
|
stream = _networkStream;
|
|
if (stream == null || _tcpClient == null || !_tcpClient.Connected)
|
|
{
|
|
shouldDelay = true;
|
|
}
|
|
}
|
|
|
|
if (shouldDelay)
|
|
{
|
|
await Task.Delay(1000, cancellationToken);
|
|
continue;
|
|
}
|
|
|
|
if (stream == null) continue;
|
|
|
|
// Read available data
|
|
if (stream.DataAvailable)
|
|
{
|
|
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
|
|
if (bytesRead > 0)
|
|
{
|
|
partialBuffer.AddRange(buffer.Take(bytesRead));
|
|
|
|
// Try to parse scan data from buffer
|
|
var parsed = TryParseScanData(partialBuffer, out var scanData, out var consumedBytes);
|
|
if (parsed)
|
|
{
|
|
// Remove consumed bytes
|
|
partialBuffer.RemoveRange(0, consumedBytes);
|
|
|
|
// Update scan data
|
|
UpdateScanData(scanData);
|
|
}
|
|
else if (partialBuffer.Count > 100000)
|
|
{
|
|
// Buffer too large, clear it
|
|
partialBuffer.Clear();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await Task.Delay(10, cancellationToken);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LastError = ex;
|
|
OnErrorOccurred(ex, "Error receiving scan data (ASCII)");
|
|
await Task.Delay(1000, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Receive scan data loop for ColaB (binary) protocol
|
|
/// Uses polling mode since scanner doesn't send data automatically
|
|
/// </summary>
|
|
private async Task ReceiveScanDataLoopColaBAsync(CancellationToken cancellationToken)
|
|
{
|
|
var partialBuffer = new List<byte>();
|
|
int loopCount = 0;
|
|
DateTime lastPollTime = DateTime.MinValue;
|
|
const int PollIntervalMs = 100; // Poll every 100ms (10 Hz)
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
loopCount++;
|
|
|
|
try
|
|
{
|
|
ColaBComm.TcpClient? tcpClient;
|
|
ColaB.ColaBSession? session;
|
|
bool shouldDelay = false;
|
|
lock (_connectionLock)
|
|
{
|
|
tcpClient = _colaBTcpClient;
|
|
session = _colaBSession;
|
|
if (tcpClient == null || !tcpClient.IsConnected || session == null || !session.IsOpen)
|
|
{
|
|
shouldDelay = true;
|
|
}
|
|
}
|
|
|
|
if (shouldDelay)
|
|
{
|
|
await Task.Delay(1000, cancellationToken);
|
|
continue;
|
|
}
|
|
|
|
if (tcpClient == null || session == null)
|
|
{
|
|
await Task.Delay(100, cancellationToken);
|
|
continue;
|
|
}
|
|
|
|
// Poll for scan data periodically (since scanner doesn't send automatically)
|
|
var timeSinceLastPoll = (DateTime.UtcNow - lastPollTime).TotalMilliseconds;
|
|
if (timeSinceLastPoll >= PollIntervalMs)
|
|
{
|
|
try
|
|
{
|
|
// Poll for scan data using sRN LMDscandata command
|
|
var readCmd = new ColaBCommands.ReadCommand("LMDscandata");
|
|
session.ExecuteCommand(readCmd, _commandTimeoutMs);
|
|
|
|
if (readCmd.WasSuccessful && readCmd.RawReplyData != null && readCmd.RawReplyData.Length > 0)
|
|
{
|
|
// Try to parse binary format
|
|
if (TryParseScanDataFromColaBBinary(readCmd.RawReplyData, out var parsedScanBinary))
|
|
{
|
|
UpdateScanData(parsedScanBinary);
|
|
}
|
|
else
|
|
{
|
|
// Fallback to ASCII parser
|
|
var replyString = System.Text.Encoding.ASCII.GetString(readCmd.RawReplyData);
|
|
var parsedScan = ParseLmdScandata(replyString);
|
|
if (parsedScan.HasValue && parsedScan.Value.Ranges != null)
|
|
{
|
|
UpdateScanData(parsedScan.Value);
|
|
}
|
|
}
|
|
|
|
lastPollTime = DateTime.UtcNow;
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
// Also try to receive any spontaneous data (in case scanner starts sending automatically)
|
|
try
|
|
{
|
|
var frame = tcpClient.Receive(50); // Very short timeout for non-blocking
|
|
|
|
// Try to parse ColaB frame
|
|
if (ColaBHelpers.ColaBHelper.TryParseFrame(frame, out var commandData, out var consumedBytes))
|
|
{
|
|
// Decode command to check type
|
|
var commandString = ColaBHelpers.ColaBHelper.DecodeCommand(commandData);
|
|
|
|
// Check if this is scan data (LMDscandata)
|
|
if (commandString.Contains("LMDscandata", StringComparison.Ordinal))
|
|
{
|
|
// Try to parse binary format first
|
|
var parsed = TryParseScanDataFromColaBBinary(commandData, out var scanData);
|
|
if (!parsed)
|
|
{
|
|
// Fallback to ASCII parser
|
|
parsed = TryParseScanDataFromColaB(commandData, out scanData);
|
|
}
|
|
|
|
if (parsed && scanData.Ranges != null && scanData.Ranges.Length > 0)
|
|
{
|
|
UpdateScanData(scanData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
// No spontaneous data available - this is normal, we're using polling
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnErrorOccurred(ex, "Error receiving scan data (ColaB)");
|
|
await Task.Delay(1000, cancellationToken);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LastError = ex;
|
|
OnErrorOccurred(ex, "Error receiving scan data (ColaB)");
|
|
await Task.Delay(1000, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to parse scan data from ColaB binary command data
|
|
/// This parses the binary format directly
|
|
/// </summary>
|
|
private bool TryParseScanDataFromColaBBinary(ReadOnlySpan<byte> commandData, out LaserScan scanData)
|
|
{
|
|
scanData = default(LaserScan);
|
|
try
|
|
{
|
|
// Binary LMDscandata format:
|
|
// Starts with ASCII header: "sRA LMDscandata " or "sSN LMDscandata "
|
|
// Then binary data follows:
|
|
// - Various fields (version, device number, etc.)
|
|
// - "DIST1" (5 bytes ASCII) followed by:
|
|
// - Number of items (2 bytes, big endian uint16)
|
|
// - Distance values (each 2 bytes, big endian uint16, units: mm)
|
|
// - "RSSI1" (5 bytes ASCII) followed by:
|
|
// - Number of items (2 bytes, big endian uint16)
|
|
// - RSSI values (each 1 or 2 bytes depending on resolution)
|
|
|
|
if (commandData.Length < 50)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if it contains "LMDscandata"
|
|
var commandStart = Encoding.ASCII.GetString(commandData.Slice(0, Math.Min(50, commandData.Length)));
|
|
if (!commandStart.Contains("LMDscandata", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Parse header fields before DIST1
|
|
// According to SICK documentation, binary LMDscandata format has:
|
|
// - Version, device number, serial number, status, etc. (variable length)
|
|
// - Scanning frequency (field 16)
|
|
// - Measurement frequency (field 17)
|
|
// - Number of 16-bit channels (field 19)
|
|
// - Measured data contents (field 20, e.g., "DIST1")
|
|
// - Scaling factor (field 21, 4 bytes float)
|
|
// - Scaling offset (field 22, 4 bytes float, usually 0)
|
|
// - Starting angle (field 23, 4 bytes signed int, units: 1/10000 degree)
|
|
// - Angular step width (field 24, 2 bytes unsigned short, units: 1/10000 degree)
|
|
// - Number of data (field 25, 2 bytes unsigned short)
|
|
|
|
float startAngleDeg = 0.0f;
|
|
float angularStepDeg = 0.0f;
|
|
float scaleFactor = 1.0f;
|
|
float scaleOffset = 0.0f;
|
|
|
|
// Find "DIST1" in the data
|
|
var dist1Pattern = Encoding.ASCII.GetBytes("DIST1");
|
|
var dist1Index = -1;
|
|
for (int i = 0; i <= commandData.Length - dist1Pattern.Length; i++)
|
|
{
|
|
bool found = true;
|
|
for (int j = 0; j < dist1Pattern.Length; j++)
|
|
{
|
|
if (commandData[i + j] != dist1Pattern[j])
|
|
{
|
|
found = false;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
{
|
|
dist1Index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (dist1Index == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try to parse header fields before DIST1
|
|
// Look for scaling factor, starting angle, and angular step before DIST1
|
|
// These are typically in the 20-30 bytes before DIST1
|
|
int headerSearchStart = Math.Max(0, dist1Index - 40);
|
|
for (int i = headerSearchStart; i < dist1Index - 10; i++)
|
|
{
|
|
// Try to find scaling factor (4 bytes float, big endian)
|
|
if (i + 4 <= dist1Index)
|
|
{
|
|
// Check if this looks like a float (scaling factor is usually 1.0 = 0x3F800000)
|
|
uint scaleFactorInt = (uint)((commandData[i] << 24) | (commandData[i + 1] << 16) | (commandData[i + 2] << 8) | commandData[i + 3]);
|
|
float testScale = BitConverter.ToSingle(BitConverter.GetBytes(scaleFactorInt), 0);
|
|
if (testScale > 0.1f && testScale < 10.0f)
|
|
{
|
|
scaleFactor = testScale;
|
|
|
|
// Scaling offset is next (4 bytes, usually 0)
|
|
if (i + 8 <= dist1Index)
|
|
{
|
|
uint scaleOffsetInt = (uint)((commandData[i + 4] << 24) | (commandData[i + 5] << 16) | (commandData[i + 6] << 8) | commandData[i + 7]);
|
|
scaleOffset = BitConverter.ToSingle(BitConverter.GetBytes(scaleOffsetInt), 0);
|
|
|
|
// Starting angle is next (4 bytes signed int, units: 1/10000 degree)
|
|
if (i + 12 <= dist1Index)
|
|
{
|
|
int startAngleInt = (int)((commandData[i + 8] << 24) | (commandData[i + 9] << 16) | (commandData[i + 10] << 8) | commandData[i + 11]);
|
|
startAngleDeg = startAngleInt / 10000.0f;
|
|
|
|
// Angular step width is next (2 bytes unsigned short, units: 1/10000 degree)
|
|
if (i + 14 <= dist1Index)
|
|
{
|
|
ushort angularStepInt = (ushort)((commandData[i + 12] << 8) | commandData[i + 13]);
|
|
angularStepDeg = angularStepInt / 10000.0f;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// According to SICK documentation and C++ reference code:
|
|
// After "DIST1" (5 bytes), there are exactly 19 bytes of header fields:
|
|
// - Scale factor (4 bytes float, big endian)
|
|
// - Scale offset (4 bytes float, big endian)
|
|
// - Starting angle (4 bytes signed int, big endian, units: 1/10000 degree)
|
|
// - Angular step width (2 bytes unsigned short, big endian, units: 1/10000 degree)
|
|
// - Reserved (5 bytes)
|
|
// Then: Count (2 bytes, big endian) at offset DIST1 + 5 + 19 = DIST1 + 24
|
|
|
|
// Parse header fields at fixed offsets
|
|
int headerStart = dist1Index + 5; // Right after "DIST1"
|
|
|
|
// Scale factor (4 bytes float, big endian) at offset headerStart
|
|
if (headerStart + 4 <= commandData.Length)
|
|
{
|
|
uint scaleFactorInt = (uint)((commandData[headerStart] << 24) | (commandData[headerStart + 1] << 16) |
|
|
(commandData[headerStart + 2] << 8) | commandData[headerStart + 3]);
|
|
scaleFactor = BitConverter.ToSingle(BitConverter.GetBytes(scaleFactorInt), 0);
|
|
}
|
|
|
|
// Scale offset (4 bytes float, big endian) at offset headerStart + 4
|
|
if (headerStart + 8 <= commandData.Length)
|
|
{
|
|
uint scaleOffsetInt = (uint)((commandData[headerStart + 4] << 24) | (commandData[headerStart + 5] << 16) |
|
|
(commandData[headerStart + 6] << 8) | commandData[headerStart + 7]);
|
|
scaleOffset = BitConverter.ToSingle(BitConverter.GetBytes(scaleOffsetInt), 0);
|
|
}
|
|
|
|
// Starting angle (4 bytes signed int, big endian) at offset headerStart + 8
|
|
if (headerStart + 12 <= commandData.Length)
|
|
{
|
|
int startAngleInt = (int)((commandData[headerStart + 8] << 24) | (commandData[headerStart + 9] << 16) |
|
|
(commandData[headerStart + 10] << 8) | commandData[headerStart + 11]);
|
|
startAngleDeg = startAngleInt / 10000.0f;
|
|
}
|
|
|
|
// Angular step width (2 bytes unsigned short, big endian) at offset headerStart + 12
|
|
if (headerStart + 14 <= commandData.Length)
|
|
{
|
|
ushort angularStepInt = (ushort)((commandData[headerStart + 12] << 8) | commandData[headerStart + 13]);
|
|
angularStepDeg = angularStepInt / 10000.0f;
|
|
}
|
|
|
|
// Count (2 bytes, big endian)
|
|
// From hex data analysis: after DIST1 (5) + scale factor (4) + scale offset (4) + starting angle (4) + angular step (2) = 19 bytes
|
|
// But actual data shows count at offset 75 from start, which is DIST1 (56) + 5 + 14 = 75
|
|
// So it seems: DIST1 (5) + scale factor (4) + scale offset (4) + starting angle (4) + angular step (2) = 19, but count is at +14, not +19
|
|
// Let's try both offsets and use the one that makes sense
|
|
|
|
ushort distCount = 0;
|
|
int distCountOffset = -1;
|
|
bool countFound = false;
|
|
|
|
// Try offset +14 first (matches actual data)
|
|
int tryOffset1 = headerStart + 14; // DIST1 + 5 + 14
|
|
if (tryOffset1 + 2 <= commandData.Length)
|
|
{
|
|
ushort testCount = (ushort)((commandData[tryOffset1] << 8) | commandData[tryOffset1 + 1]);
|
|
if (testCount >= 50 && testCount <= 5000)
|
|
{
|
|
int requiredBytes = tryOffset1 + 2 + (testCount * 2);
|
|
if (requiredBytes <= commandData.Length)
|
|
{
|
|
distCount = testCount;
|
|
distCountOffset = tryOffset1;
|
|
countFound = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try offset +19 (per C++ reference code)
|
|
if (!countFound)
|
|
{
|
|
int tryOffset2 = headerStart + 19; // DIST1 + 5 + 19
|
|
if (tryOffset2 + 2 <= commandData.Length)
|
|
{
|
|
ushort testCount = (ushort)((commandData[tryOffset2] << 8) | commandData[tryOffset2 + 1]);
|
|
if (testCount >= 50 && testCount <= 5000)
|
|
{
|
|
int requiredBytes = tryOffset2 + 2 + (testCount * 2);
|
|
if (requiredBytes <= commandData.Length)
|
|
{
|
|
distCount = testCount;
|
|
distCountOffset = tryOffset2;
|
|
countFound = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If fixed offset doesn't work, try searching (fallback)
|
|
if (!countFound)
|
|
{
|
|
int searchStart = dist1Index + 5;
|
|
int searchEnd = Math.Min(dist1Index + 5 + 30, commandData.Length - 2);
|
|
|
|
for (int offset = searchStart; offset <= searchEnd && !countFound; offset++)
|
|
{
|
|
// Try big endian
|
|
if (offset + 2 <= commandData.Length)
|
|
{
|
|
ushort testCount = (ushort)((commandData[offset] << 8) | commandData[offset + 1]);
|
|
|
|
if (testCount >= 50 && testCount <= 5000)
|
|
{
|
|
int requiredBytes = offset + 2 + (testCount * 2);
|
|
if (requiredBytes <= commandData.Length)
|
|
{
|
|
distCount = testCount;
|
|
distCountOffset = offset;
|
|
countFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!countFound)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Read distance values (each 2 bytes, big endian, units: mm)
|
|
var distValuesOffset = distCountOffset + 2;
|
|
if (distValuesOffset + (distCount * 2) > commandData.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var ranges = new List<double>();
|
|
|
|
for (int i = 0; i < distCount; i++)
|
|
{
|
|
var offset = distValuesOffset + (i * 2);
|
|
if (offset + 2 > commandData.Length)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Read uint16 big endian
|
|
ushort distValue = (ushort)((commandData[offset] << 8) | commandData[offset + 1]);
|
|
// Apply scale factor and convert from mm to meters
|
|
float distanceM = (distValue * scaleFactor + scaleOffset) / 1000.0f;
|
|
ranges.Add(distanceM);
|
|
}
|
|
|
|
// Find "RSSI1" for intensities (optional)
|
|
var rssi1Pattern = Encoding.ASCII.GetBytes("RSSI1");
|
|
var rssi1Index = -1;
|
|
var intensities = new List<double>();
|
|
|
|
// Search after DIST1 data
|
|
var rssiSearchStart = distValuesOffset + (distCount * 2);
|
|
for (int i = rssiSearchStart; i <= commandData.Length - rssi1Pattern.Length; i++)
|
|
{
|
|
bool found = true;
|
|
for (int j = 0; j < rssi1Pattern.Length; j++)
|
|
{
|
|
if (commandData[i + j] != rssi1Pattern[j])
|
|
{
|
|
found = false;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
{
|
|
rssi1Index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (rssi1Index != -1)
|
|
{
|
|
// RSSI data is BINARY after "RSSI1" header
|
|
// Format appears to be: "RSSI1" + <2-byte scaling 3F80> + ... + 0x2B + <RSSI data>
|
|
// Based on hex observation, RSSI values start after finding 0x2B separator
|
|
int rssiDataOffset = rssi1Index + 5; // After "RSSI1"
|
|
|
|
// Search for 0x2B separator (observed pattern in all telegrams)
|
|
bool found2B = false;
|
|
for (int searchOffset = rssiDataOffset; searchOffset < Math.Min(rssiDataOffset + 50, commandData.Length); searchOffset++)
|
|
{
|
|
if (commandData[searchOffset] == 0x2B)
|
|
{
|
|
rssiDataOffset = searchOffset + 1; // Start after 0x2B
|
|
found2B = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found2B)
|
|
{
|
|
rssiDataOffset = rssi1Index + 7; // Fallback
|
|
}
|
|
|
|
var rssiBytesAvailable = commandData.Length - rssiDataOffset;
|
|
if (rssiBytesAvailable > 0)
|
|
{
|
|
// RSSI can be encoded as 16-bit or 8-bit values.
|
|
// Use 16-bit when enough bytes are available; fallback to 8-bit otherwise.
|
|
var hasFull16BitPayload = rssiBytesAvailable >= (ranges.Count * 2);
|
|
|
|
if (hasFull16BitPayload)
|
|
{
|
|
var rssiCount16 = Math.Min(ranges.Count, rssiBytesAvailable / 2);
|
|
for (int i = 0; i < rssiCount16; i++)
|
|
{
|
|
var offset = rssiDataOffset + (i * 2);
|
|
ushort rssiValue = (ushort)((commandData[offset] << 8) | commandData[offset + 1]);
|
|
intensities.Add(rssiValue);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var rssiCount8 = Math.Min(ranges.Count, rssiBytesAvailable);
|
|
for (int i = 0; i < rssiCount8; i++)
|
|
{
|
|
byte rssiValue = commandData[rssiDataOffset + i];
|
|
intensities.Add(rssiValue);
|
|
}
|
|
}
|
|
|
|
if (ranges.Count - intensities.Count > 0)
|
|
{
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI] WARNING: Only parsed {intensities.Count}/{ranges.Count} RSSI values (telegram truncated)");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI] ERROR: No RSSI data available (offset: {rssiDataOffset}, length: {commandData.Length})");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// RSSI1 field NOT found in telegram - log only once per 100 scans to avoid spam
|
|
if (ranges.Count > 0 && (ranges.Count % 100 == 0))
|
|
{
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI] RSSI1 field NOT FOUND. Telegram size: {commandData.Length} bytes, DIST count: {ranges.Count}");
|
|
}
|
|
}
|
|
|
|
if (ranges.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Calculate angle parameters
|
|
// Use parsed values if available, otherwise use defaults
|
|
float angleMinRad, angleMaxRad, angleIncrementRad;
|
|
|
|
if (angularStepDeg > 0.0f && startAngleDeg != 0.0f)
|
|
{
|
|
// Use parsed angle information
|
|
angleMinRad = (float)(startAngleDeg * Math.PI / 180.0);
|
|
angleIncrementRad = (float)(angularStepDeg * Math.PI / 180.0);
|
|
angleMaxRad = angleMinRad + (ranges.Count - 1) * angleIncrementRad;
|
|
}
|
|
else
|
|
{
|
|
// Fallback to calculated values
|
|
var angleSpan = _maxAngleRad - _minAngleRad;
|
|
angleMinRad = (float)_minAngleRad;
|
|
angleMaxRad = (float)_maxAngleRad;
|
|
angleIncrementRad = ranges.Count > 1 ? (float)(angleSpan / (ranges.Count - 1)) : (float)(_angularResolutionRad ?? (Math.PI / 180.0));
|
|
}
|
|
|
|
// Create scan
|
|
scanData = new LaserScan
|
|
{
|
|
Header = new Header
|
|
{
|
|
Stamp = DateTime.UtcNow,
|
|
FrameId = _frameId
|
|
},
|
|
AngleMin = angleMinRad,
|
|
AngleMax = angleMaxRad,
|
|
AngleIncrement = angleIncrementRad,
|
|
TimeIncrement = 0.0f,
|
|
ScanTime = _scanFrequencyHz.HasValue ? (float)(1.0 / _scanFrequencyHz.Value) : 0.1f,
|
|
RangeMin = (float)_minRangeM,
|
|
RangeMax = (float)_maxRangeM,
|
|
Ranges = ranges.ToArray(),
|
|
Intensities = intensities.Count > 0 ? intensities.ToArray() : null
|
|
};
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnErrorOccurred(ex, "Error parsing binary scan data from ColaB");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to parse scan data from ColaB command data (ASCII fallback)
|
|
/// </summary>
|
|
private bool TryParseScanDataFromColaB(ReadOnlySpan<byte> commandData, out LaserScan scanData)
|
|
{
|
|
scanData = default(LaserScan);
|
|
|
|
try
|
|
{
|
|
// Decode command string to check if it's scan data
|
|
var commandString = ColaBHelpers.ColaBHelper.DecodeCommand(commandData);
|
|
|
|
// Check if this is scan data telegram
|
|
if (!commandString.Contains("LMDscandata", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try to parse using existing ASCII parser as fallback
|
|
var commandStringBytes = commandData.ToArray();
|
|
var buffer = new List<byte>(commandStringBytes);
|
|
|
|
if (TryParseScanData(buffer, out var parsedScanData, out _))
|
|
{
|
|
scanData = parsedScanData;
|
|
return true;
|
|
}
|
|
|
|
// If ASCII parsing fails, try to parse the raw string directly
|
|
if (ParseLmdScandata(commandString) is { } parsedData)
|
|
{
|
|
scanData = parsedData;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnErrorOccurred(ex, "Error parsing scan data from ColaB");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to parse scan data from buffer
|
|
/// This is a simplified parser - real implementation should handle binary protocol properly
|
|
/// </summary>
|
|
private bool TryParseScanData(List<byte> buffer, out LaserScan scanData, out int consumedBytes)
|
|
{
|
|
scanData = default(LaserScan);
|
|
consumedBytes = 0;
|
|
|
|
if (buffer.Count < 20)
|
|
return false;
|
|
|
|
// Look for scan data telegram
|
|
// For ASCII: "sSN LMDscandata ..."
|
|
// For binary: STX STX STX STX + length + data + CRC
|
|
|
|
bool useBinary;
|
|
lock (_protocolLock)
|
|
{
|
|
useBinary = _actualUseBinaryProtocol;
|
|
}
|
|
|
|
if (useBinary)
|
|
{
|
|
// Binary protocol parsing
|
|
// Look for STX STX STX STX (0x02 0x02 0x02 0x02)
|
|
var stxIndex = -1;
|
|
for (int i = 0; i <= buffer.Count - 4; i++)
|
|
{
|
|
if (buffer[i] == 0x02 && buffer[i + 1] == 0x02 &&
|
|
buffer[i + 2] == 0x02 && buffer[i + 3] == 0x02)
|
|
{
|
|
stxIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (stxIndex < 0)
|
|
return false;
|
|
|
|
// Parse binary telegram (simplified - real implementation needs full parser)
|
|
// This is a placeholder - actual parsing should be implemented based on SICK scanner documentation
|
|
// For now, we'll use a basic ASCII parser as fallback
|
|
}
|
|
|
|
// ASCII protocol parsing
|
|
var bufferStr = Encoding.ASCII.GetString(buffer.ToArray());
|
|
var scanDataIndex = bufferStr.IndexOf("sSN LMDscandata", StringComparison.Ordinal);
|
|
if (scanDataIndex < 0)
|
|
{
|
|
// Also check for "sAN LMDscandata" (answer format)
|
|
scanDataIndex = bufferStr.IndexOf("sAN LMDscandata", StringComparison.Ordinal);
|
|
}
|
|
|
|
if (scanDataIndex < 0)
|
|
{
|
|
// Try to find end of current message to consume bytes
|
|
var etxIndex = buffer.IndexOf(0x03);
|
|
if (etxIndex >= 0)
|
|
{
|
|
consumedBytes = etxIndex + 1;
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Find end of message (ETX or next STX)
|
|
var messageStart = scanDataIndex;
|
|
var messageEnd = bufferStr.IndexOf('\x03', messageStart);
|
|
if (messageEnd < 0)
|
|
{
|
|
// Message not complete yet
|
|
return false;
|
|
}
|
|
|
|
consumedBytes = messageEnd + 1;
|
|
|
|
// Parse scan data (simplified - real implementation needs full parser)
|
|
// This is a basic implementation - should be enhanced with proper parsing
|
|
try
|
|
{
|
|
var parsedData = ParseLmdScandata(bufferStr.Substring(messageStart, messageEnd - messageStart));
|
|
if (parsedData.HasValue)
|
|
{
|
|
scanData = parsedData.Value;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse LMDscandata telegram (simplified parser)
|
|
/// Real implementation should handle all fields properly
|
|
/// </summary>
|
|
private LaserScan? ParseLmdScandata(string telegram)
|
|
{
|
|
Console.WriteLine($"[SICK-PARSER] ParseLmdScandata (ASCII) called, telegram length: {telegram.Length}");
|
|
|
|
try
|
|
{
|
|
// Split telegram into fields (space-separated)
|
|
var fields = telegram.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (fields.Length < 20)
|
|
{
|
|
OnErrorOccurred(new InvalidOperationException("Telegram too short"),
|
|
$"LMDscandata telegram has only {fields.Length} fields, expected at least 20");
|
|
return null;
|
|
}
|
|
|
|
// Find DIST1 field and parse distances
|
|
// Format: "sSN LMDscandata <version> <device> <serial> <status> <counter> <scan> ... DIST1 <count> <distances> ... RSSI1 <count> <intensities> ..."
|
|
var ranges = new List<double>();
|
|
var intensities = new List<double>();
|
|
|
|
int distIndex = -1;
|
|
int rssiIndex = -1;
|
|
|
|
// Find DIST1 field
|
|
for (int i = 0; i < fields.Length; i++)
|
|
{
|
|
if (fields[i].StartsWith("DIST", StringComparison.Ordinal))
|
|
{
|
|
distIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find RSSI1 field
|
|
for (int i = 0; i < fields.Length; i++)
|
|
{
|
|
if (fields[i].StartsWith("RSSI", StringComparison.Ordinal))
|
|
{
|
|
rssiIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Parse distances
|
|
if (distIndex >= 0 && distIndex + 1 < fields.Length)
|
|
{
|
|
// Next field after DIST1 is the count (in hex)
|
|
if (int.TryParse(fields[distIndex + 1], System.Globalization.NumberStyles.HexNumber, null, out int distCount))
|
|
{
|
|
// Parse distance values (in hex, units are mm, convert to meters)
|
|
for (int i = 0; i < distCount && distIndex + 2 + i < fields.Length; i++)
|
|
{
|
|
if (int.TryParse(fields[distIndex + 2 + i], System.Globalization.NumberStyles.HexNumber, null, out int distValue))
|
|
{
|
|
// Convert from mm to meters
|
|
ranges.Add(distValue / 1000.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse intensities (RSSI)
|
|
if (rssiIndex >= 0 && rssiIndex + 1 < fields.Length)
|
|
{
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI-ASCII] Found RSSI at index {rssiIndex}");
|
|
|
|
// Next field after RSSI1 is the count (in hex)
|
|
if (int.TryParse(fields[rssiIndex + 1], System.Globalization.NumberStyles.HexNumber, null, out int rssiCount))
|
|
{
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI-ASCII] Parsing {rssiCount} RSSI values");
|
|
|
|
// Parse RSSI values (in hex)
|
|
for (int i = 0; i < rssiCount && rssiIndex + 2 + i < fields.Length; i++)
|
|
{
|
|
if (int.TryParse(fields[rssiIndex + 2 + i], System.Globalization.NumberStyles.HexNumber, null, out int rssiValue))
|
|
{
|
|
intensities.Add(rssiValue);
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI-ASCII] Parsed {intensities.Count} RSSI values successfully");
|
|
}
|
|
}
|
|
else if (ranges.Count > 0 && ranges.Count % 100 == 0)
|
|
{
|
|
Console.WriteLine($"[SICK-LIDAR-RSSI-ASCII] RSSI field NOT FOUND. Fields: {fields.Length}, RSSI index: {rssiIndex}");
|
|
}
|
|
|
|
// If no ranges found, return null
|
|
if (ranges.Count == 0)
|
|
{
|
|
OnErrorOccurred(new InvalidOperationException("No distance data found"),
|
|
$"LMDscandata telegram does not contain distance data. Fields: {string.Join(" ", fields.Take(30))}");
|
|
return null;
|
|
}
|
|
|
|
// Calculate angle parameters from ranges count
|
|
var angleSpan = _maxAngleRad - _minAngleRad;
|
|
var angleIncrement = ranges.Count > 1
|
|
? (float)(angleSpan / (ranges.Count - 1))
|
|
: (_angularResolutionRad.HasValue ? (float)_angularResolutionRad.Value : (float)(Math.PI / 180.0));
|
|
|
|
// Create scan
|
|
var scan = new LaserScan
|
|
{
|
|
Header = new Header
|
|
{
|
|
Stamp = DateTime.UtcNow,
|
|
FrameId = _frameId
|
|
},
|
|
AngleMin = (float)_minAngleRad,
|
|
AngleMax = (float)_maxAngleRad,
|
|
AngleIncrement = angleIncrement,
|
|
TimeIncrement = 0.0f,
|
|
ScanTime = _scanFrequencyHz.HasValue ? (float)(1.0 / _scanFrequencyHz.Value) : 0.1f,
|
|
RangeMin = (float)_minRangeM,
|
|
RangeMax = (float)_maxRangeM,
|
|
Ranges = ranges.ToArray(),
|
|
Intensities = intensities.Count > 0 ? intensities.ToArray() : null
|
|
};
|
|
|
|
return scan;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnErrorOccurred(ex, $"Error parsing LMDscandata telegram: {telegram.Substring(0, Math.Min(200, telegram.Length))}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update scan data and fire event
|
|
/// </summary>
|
|
private void UpdateScanData(LaserScan scanData)
|
|
{
|
|
lock (_scanDataLock)
|
|
{
|
|
_lastScanData = scanData;
|
|
_lastScanDataTimestamp = DateTime.UtcNow;
|
|
}
|
|
|
|
// Fire ILidar event (this notifies subscribers like web UI)
|
|
var eventArgs = new LidarScanDataEventArgs(_lastScanDataTimestamp.Value, scanData);
|
|
ScanDataReceived?.Invoke(this, eventArgs);
|
|
|
|
// Update properties for UI
|
|
SetProperty("LastScanDataTimestamp", _lastScanDataTimestamp.Value.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ILidar Implementation
|
|
|
|
public LaserScan? CurrentMeasurementData
|
|
{
|
|
get
|
|
{
|
|
lock (_scanDataLock)
|
|
{
|
|
return _lastScanData;
|
|
}
|
|
}
|
|
}
|
|
|
|
public DateTime? LastScanDataTimestamp
|
|
{
|
|
get
|
|
{
|
|
lock (_scanDataLock)
|
|
{
|
|
return _lastScanDataTimestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
public double MinAngleRad => _minAngleRad;
|
|
|
|
public double MaxAngleRad => _maxAngleRad;
|
|
|
|
public double MinRangeM => _minRangeM;
|
|
|
|
public double MaxRangeM => _maxRangeM;
|
|
|
|
public double? AngularResolutionRad => _angularResolutionRad;
|
|
|
|
public double? ScanFrequencyHz => _scanFrequencyHz;
|
|
|
|
public double FieldOfViewRad => Math.Abs(_maxAngleRad - _minAngleRad);
|
|
|
|
public bool SupportsIntensity => _supportsIntensity;
|
|
|
|
public double? AccuracyM => null; // Not specified
|
|
|
|
public event EventHandler<LidarScanDataEventArgs>? ScanDataReceived;
|
|
|
|
#endregion
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_receiveCts?.Cancel();
|
|
_receiveTask?.Wait(1000);
|
|
_receiveCts?.Dispose();
|
|
_receiveTask?.Dispose();
|
|
|
|
DisconnectInternal();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|
|
|