using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using System.Net.Sockets; namespace Olei.LidarSensor; /// /// High-performance UDP server for receiving and parsing Olei LiDAR data /// Uses Socket API with pre-allocated buffers for zero-allocation receive /// Optimized for low latency and minimal memory allocation /// public sealed class OleiLidarServer : IDisposable { private readonly int _port; private Socket? _socket; private Thread? _receiveThread; private volatile bool _isRunning; private readonly Lock _lock = new(); // Pre-allocated receive buffer (reused across all receives) private byte[]? _receiveBuffer; private const int RECEIVE_BUFFER_SIZE = LidarDataPacket.PACKET_SIZE * 16; // Buffer for multiple packets // Packet pool for reuse private readonly ConcurrentBag _packetPool; private const int INITIAL_PACKET_POOL_SIZE = 10; private const int MAX_PACKET_POOL_SIZE = 50; // Pool size tracking (atomic counter for thread-safe accurate tracking) private long _packetPoolCount; // Statistics private long _totalPacketsReceived; private long _totalPacketsParsed; private long _totalParseErrors; private long _totalPacketsCreated; // Scan frequency calculation (optimized - update only once per second) private readonly Stopwatch _frequencyStopwatch = new(); private long _packetsReceivedInCurrentSecond; private long _lastFrequencyUpdateMs; /// /// Event raised when a valid LiDAR packet is received and parsed /// public event EventHandler? DataReceived; /// /// Event raised when an error occurs during packet reception or parsing /// public event EventHandler? ErrorOccurred; /// /// Total number of packets received (including invalid ones) /// public long TotalPacketsReceived => Interlocked.Read(ref _totalPacketsReceived); /// /// Total number of packets successfully parsed /// public long TotalPacketsParsed => Interlocked.Read(ref _totalPacketsParsed); /// /// Total number of parse errors /// public long TotalParseErrors => Interlocked.Read(ref _totalParseErrors); /// /// Total number of packets created (for monitoring allocation rate) /// public long TotalPacketsCreated => Interlocked.Read(ref _totalPacketsCreated); /// /// Current number of packets in the pool /// public long CurrentPoolSize => Interlocked.Read(ref _packetPoolCount); /// /// Scan frequency (Hz) /// Typically 10-20 Hz for Olei LiDAR /// public double? ScanFrequencyHz { get; private set; } /// /// Check if server is currently running /// public bool IsRunning => _isRunning; /// /// Create a new Olei LiDAR server /// /// UDP port to listen on public OleiLidarServer(int port) { _port = port; _packetPool = []; _packetPoolCount = 0; _totalPacketsCreated = 0; // Pre-allocate packet pool for (int i = 0; i < INITIAL_PACKET_POOL_SIZE; i++) { _packetPool.Add(new LidarDataPacket()); Interlocked.Increment(ref _packetPoolCount); Interlocked.Increment(ref _totalPacketsCreated); } } /// /// Start receiving LiDAR data /// public void Start() { lock (_lock) { if (_isRunning) return; try { // Create socket with optimized settings _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); _socket.Bind(new IPEndPoint(IPAddress.Any, _port)); // Set socket options for high-performance UDP reception _socket.ReceiveBufferSize = 2 * 1024 * 1024; // 2MB OS buffer _socket.ReceiveTimeout = 0; // Blocking receive (no timeout) // Allocate receive buffer once _receiveBuffer = new byte[RECEIVE_BUFFER_SIZE]; _isRunning = true; // Reset frequency calculation _packetsReceivedInCurrentSecond = 0; _lastFrequencyUpdateMs = 0; _frequencyStopwatch.Restart(); // Start dedicated receive thread with high priority _receiveThread = new Thread(ReceiveLoop) { Name = $"OleiLidar-Receive-{_port}", IsBackground = false, Priority = ThreadPriority.Highest }; _receiveThread.Start(); Console.WriteLine($"[OleiLidarServer] Started on port {_port}"); } catch (Exception ex) { _isRunning = false; OnError(new LidarErrorEventArgs($"Failed to start server: {ex.Message}", ex)); throw; } } } /// /// Stop receiving LiDAR data /// public void Stop() { lock (_lock) { if (!_isRunning) return; _isRunning = false; try { _socket?.Close(); _socket?.Dispose(); _socket = null; // Wait for receive thread to exit (with timeout) _receiveThread?.Join(TimeSpan.FromSeconds(2)); _receiveThread = null; // Clean up receive buffer _receiveBuffer = null; _frequencyStopwatch.Stop(); Console.WriteLine($"[OleiLidarServer] Stopped on port {_port}"); } catch (Exception ex) { OnError(new LidarErrorEventArgs($"Error stopping server: {ex.Message}", ex)); } } } /// /// Main receive loop running in dedicated thread /// Uses blocking Socket.ReceiveFrom with pre-allocated buffer for zero-allocation receive /// private void ReceiveLoop() { Thread.BeginThreadAffinity(); try { EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); while (_isRunning) { try { if (_socket == null || _receiveBuffer == null) break; // Blocking receive into pre-allocated buffer (zero allocation!) int receivedBytes = _socket.ReceiveFrom(_receiveBuffer, ref remoteEndPoint); if (receivedBytes <= 0) continue; // Process ALL packets in received buffer int offset = 0; while (offset + LidarDataPacket.PACKET_SIZE <= receivedBytes) { // Create span directly from pre-allocated buffer (zero-copy) ReadOnlySpan packetData = _receiveBuffer.AsSpan(offset, LidarDataPacket.PACKET_SIZE); // Quick validation before processing if (LidarPacketParser.IsValidPacket(packetData)) { Interlocked.Increment(ref _totalPacketsReceived); ProcessPacketSpan(packetData); } offset += LidarDataPacket.PACKET_SIZE; } // Note: Leftover bytes warning removed from hot path for performance } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) { // Socket was closed, exit loop break; } catch (ObjectDisposedException) { // Socket was disposed, exit loop break; } catch (Exception ex) { OnError(new LidarErrorEventArgs($"Error receiving packet: {ex.Message}", ex)); } } } finally { Thread.EndThreadAffinity(); } } /// /// Process received packet from ReadOnlySpan (zero-copy) /// private void ProcessPacketSpan(ReadOnlySpan data) { LidarDataPacket? packet = null; try { // Get packet from pool or create new one if (_packetPool.TryTake(out packet)) { Interlocked.Decrement(ref _packetPoolCount); } else { packet = new LidarDataPacket(); Interlocked.Increment(ref _totalPacketsCreated); } // Parse packet (already validated) if (LidarPacketParser.TryParse(data, packet)) { Interlocked.Increment(ref _totalPacketsParsed); // Update scan frequency (optimized - only check timestamp, no lock) UpdateScanFrequencyOptimized(); // Raise event and automatically return packet to pool after processing OnDataReceived(packet); } else { Interlocked.Increment(ref _totalParseErrors); } } catch (Exception ex) { Interlocked.Increment(ref _totalParseErrors); OnError(new LidarErrorEventArgs($"Error processing packet: {ex.Message}", ex)); } finally { // Always return packet to pool, even if event handler throws exception if (packet != null) { ReturnPacketToPool(packet); } } } /// /// Update scan frequency based on packets received per second /// Optimized version: only takes lock when actually updating (once per second) /// private void UpdateScanFrequencyOptimized() { // Increment packet count (lock-free) Interlocked.Increment(ref _packetsReceivedInCurrentSecond); // Check if 1 second has elapsed (lock-free check) long elapsedMs = _frequencyStopwatch.ElapsedMilliseconds; if (elapsedMs - Interlocked.Read(ref _lastFrequencyUpdateMs) >= 1000) { // Try to update (only one thread will succeed) long previousUpdateMs = Interlocked.CompareExchange(ref _lastFrequencyUpdateMs, elapsedMs, Interlocked.Read(ref _lastFrequencyUpdateMs)); // Check if we won the race to update if (elapsedMs - previousUpdateMs >= 1000) { // Calculate frequency long packetCount = Interlocked.Exchange(ref _packetsReceivedInCurrentSecond, 0); double elapsedSeconds = (elapsedMs - previousUpdateMs) / 1000.0; ScanFrequencyHz = packetCount / elapsedSeconds; } } } /// /// Return packet to pool for reuse /// This is called automatically after event handlers complete /// Uses atomic operations to prevent race conditions and ensure accurate pool size /// private void ReturnPacketToPool(LidarDataPacket packet) { if (packet == null) return; // Atomically try to increment the pool count long newCount = Interlocked.Increment(ref _packetPoolCount); if (newCount <= MAX_PACKET_POOL_SIZE) { // Successfully reserved a slot in the pool _packetPool.Add(packet); } else { // Pool is full - decrement counter back and let packet be GC'd Interlocked.Decrement(ref _packetPoolCount); } } /// /// Raise DataReceived event /// Packet is automatically returned to pool after all handlers complete /// private void OnDataReceived(LidarDataPacket e) { DataReceived?.Invoke(this, e); } /// /// Raise ErrorOccurred event /// private void OnError(LidarErrorEventArgs e) { ErrorOccurred?.Invoke(this, e); } /// /// Reset statistics counters /// Note: Pool size and created count are preserved (shows cumulative allocations) /// public void ResetStatistics() { Interlocked.Exchange(ref _totalPacketsReceived, 0); Interlocked.Exchange(ref _totalPacketsParsed, 0); Interlocked.Exchange(ref _totalParseErrors, 0); // Reset frequency calculation Interlocked.Exchange(ref _packetsReceivedInCurrentSecond, 0); Interlocked.Exchange(ref _lastFrequencyUpdateMs, 0); _frequencyStopwatch.Restart(); ScanFrequencyHz = null; } /// /// Get statistics summary with pool metrics /// public string GetStatistics() { long received = TotalPacketsReceived; long parsed = TotalPacketsParsed; long errors = TotalParseErrors; long created = TotalPacketsCreated; long poolSize = CurrentPoolSize; double successRate = received > 0 ? (parsed * 100.0 / received) : 0; string frequency = ScanFrequencyHz.HasValue ? $"{ScanFrequencyHz.Value:F2} Hz" : "N/A"; return $"Received: {received}, Parsed: {parsed}, Errors: {errors}, " + $"Created: {created}, Pool: {poolSize}/{MAX_PACKET_POOL_SIZE}, " + $"Success: {successRate:F2}%, Frequency: {frequency}"; } /// /// Dispose resources /// public void Dispose() { Stop(); GC.SuppressFinalize(this); } } /// /// Event args for LiDAR error events /// public class LidarErrorEventArgs(string message, Exception? exception = null) : EventArgs { public string Message { get; } = message; public Exception? Exception { get; } = exception; }