using Sick.SafetyScanners.DataStructures; namespace Sick.SafetyScanners.DataProcessing; /// /// Merges UDP packets that may be fragmented across multiple UDP packets /// Thread-safe implementation - matches C++ logic /// public sealed class UdpPacketMerger : IDisposable { private readonly Dictionary> _parsedPacketBufferMap = []; private bool _isComplete = false; private PacketBuffer? _deployedBuffer; private bool _disposed; private readonly Lock _lock = new(); /// /// Indicates whether a data packet is complete /// public bool IsComplete { get { lock (_lock) { return _isComplete; } } } /// /// Indicates whether the buffer is empty /// public bool IsEmpty { get { lock (_lock) { return _parsedPacketBufferMap.Count == 0; } } } /// /// Adds a UDP packet to the merger (stores reference, doesn't copy data yet) /// Returns true if the packet is complete /// public bool AddUdpPacket(PacketBuffer buffer) { ObjectDisposedException.ThrowIf(_disposed, nameof(UdpPacketMerger)); ArgumentNullException.ThrowIfNull(buffer); lock (_lock) { if (_isComplete) { _isComplete = false; _deployedBuffer = null; } // Parse datagram header var headerParser = new ParseDatagramHeader(); var datagramHeader = headerParser.ParseUdpSequence(buffer); // Add to map AddToMap(buffer, datagramHeader); // Check if complete and deploy if so DeployPacketIfComplete(datagramHeader); return _isComplete; } } /// /// Gets the latest complete data packet /// public PacketBuffer GetDeployedPacketBuffer() { ObjectDisposedException.ThrowIf(_disposed, nameof(UdpPacketMerger)); lock (_lock) { if (!_isComplete || _deployedBuffer == null) throw new InvalidOperationException("No complete packet available"); _isComplete = false; var result = _deployedBuffer; _deployedBuffer = null; return result; } } /// /// Resets the merger (clears all buffers) /// public void Reset() { lock (_lock) { _parsedPacketBufferMap.Clear(); _isComplete = false; _deployedBuffer = null; } } private void AddToMap(PacketBuffer buffer, DatagramHeader header) { var parsedBuffer = new ParsedPacketBuffer(buffer, header); if (_parsedPacketBufferMap.TryGetValue(header.Identification, out var list)) { list.Add(parsedBuffer); } else { _parsedPacketBufferMap[header.Identification] = [parsedBuffer]; } } private void DeployPacketIfComplete(DatagramHeader header) { if (!_parsedPacketBufferMap.TryGetValue(header.Identification, out var list)) return; if (!CheckIfComplete(header, list)) return; // Sort by fragment offset var sortedList = list.OrderBy(p => p.DatagramHeader.FragmentOffset).ToList(); // Remove headers and merge data var mergedData = RemoveHeaderFromParsedPacketBuffers(sortedList); _deployedBuffer = new PacketBuffer(mergedData); _parsedPacketBufferMap.Remove(header.Identification); _isComplete = true; } private static bool CheckIfComplete(DatagramHeader header, List list) { var totalLength = header.TotalLength; var currentLength = CalculateCurrentLength(list); if (currentLength != totalLength) return false; return true; } private static uint CalculateCurrentLength(List list) { uint currentLength = 0; foreach (var parsedBuffer in list) { var packetBuffer = parsedBuffer.PacketBuffer; currentLength += (uint)(packetBuffer.GetBuffer().Length - DatagramHeader.HeaderSize); } return currentLength; } private static byte[] RemoveHeaderFromParsedPacketBuffers(List sortedList) { var result = new List(); foreach (var parsedBuffer in sortedList) { var packetBuffer = parsedBuffer.PacketBuffer; var bufferData = packetBuffer.GetBuffer(); // Skip header (first HeaderSize bytes) and add rest if (bufferData.Length > DatagramHeader.HeaderSize) { var dataWithoutHeader = bufferData.Span[DatagramHeader.HeaderSize..]; result.AddRange(dataWithoutHeader.ToArray()); } } return [.. result]; } public void Dispose() { if (_disposed) return; lock (_lock) { _parsedPacketBufferMap.Clear(); _deployedBuffer?.Dispose(); _deployedBuffer = null; } _disposed = true; } }