Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# UDP Packet Parsers Implementation Guide
## Overview
This document describes the UDP packet parsers for SICK Safety Scanner scan data. The parsers are based on the C++ reference implementation in `sick_safetyscanners_base`.
## Data Structures Created
All data structures have been created in `DataStructures/` folder:
1. **DataHeader.cs** - Header metadata (version, serial numbers, channel, sequence, scan numbers, timestamps, block offsets/sizes)
2. **ScanPoint.cs** - Single scan point (angle, distance, reflectivity, flags)
3. **MeasurementData.cs** - Collection of scan points
4. **DerivedValues.cs** - Configuration of data output (multiplication factor, number of beams, scan time, angles, resolution)
5. **GeneralSystemState.cs** - Device status (run/standby mode, cut-off paths, monitoring cases, errors)
6. **IntrusionDatum.cs** - Single intrusion datum
7. **IntrusionData.cs** - Collection of intrusion data (field interruption)
8. **ApplicationInputs.cs** - Application inputs (local inputs)
9. **ApplicationOutputs.cs** - Application outputs (local outputs)
10. **ApplicationData.cs** - Bundles application inputs and outputs
11. **UdpScanData.cs** - Complete parsed scan data containing all blocks
## Parser Classes Status
### ✅ Completed
- **ParseDataHeader.cs** - Fully implemented parser for data header
### ⏳ To Be Implemented
The following parsers need to be implemented based on C++ reference:
1. **ParseDerivedValues.cs** - Parse derived values block
- Reference: `srcs/refs/sick_safetyscanners_base/src/data_processing/ParseDerivedValues.cpp`
- Parse: multiplication factor (offset 0), number of beams (offset 2), scan time (offset 4), start angle (offset 8), angular beam resolution (offset 12), interbeam period (offset 16)
- Angle conversion: Use `DerivedValues.AngleResolution = 4194304.0` to convert from sensor units to radians
2. **ParseMeasurementData.cs** - Parse measurement data block
- Reference: `srcs/refs/sick_safetyscanners_base/src/data_processing/ParseMeasurementData.cpp`
- Parse: number of beams (offset 0), then for each beam: distance (offset 4 + i*4), reflectivity (offset 6 + i*4), status flags (offset 7 + i*4)
- Requires DerivedValues for start angle and angular resolution
- Status byte bits: bit 0=valid, bit 1=infinite, bit 2=glare, bit 3=reflector, bit 4=contamination, bit 5=contamination_warning
3. **ParseGeneralSystemState.cs** - Parse general system state block
- Reference: `srcs/refs/sick_safetyscanners_base/src/data_processing/ParseGeneralSystemState.cpp`
- Parse: status bits (offset 0), safe cut-off paths (offset 1-3), non-safe cut-off paths (offset 4-6), reset required paths (offset 7-9), monitoring cases (offset 10-13), errors (offset 15)
4. **ParseIntrusionData.cs** - Parse intrusion data block
- Reference: `srcs/refs/sick_safetyscanners_base/src/data_processing/ParseIntrusionData.cpp`
- Parse: 24 intrusion datums, each with size (4 bytes) and flags (variable size based on number of scan points)
5. **ParseApplicationData.cs** - Parse application data block
- Reference: `srcs/refs/sick_safetyscanners_base/src/data_processing/ParseApplicationData.cpp`
- Parse: ApplicationInputs (offsets 0-74) and ApplicationOutputs (offsets 140-259)
- Complex parsing of bit fields for inputs/outputs, velocities, monitoring cases, etc.
6. **ParseData.cs** - Main parser that coordinates all sub-parsers
- Reference: `srcs/refs/sick_safetyscanners_base/src/data_processing/ParseData.cpp`
- Orchestrates parsing of all blocks in order:
1. ParseDataHeader
2. ParseDerivedValues
3. ParseMeasurementData
4. ParseGeneralSystemState
5. ParseIntrusionData
6. ParseApplicationData
- Validates packet size and block offsets/sizes
## Implementation Notes
### Endianness
- All values are read in **Little Endian** format
- Use `ReadWriteHelper.ReadUint8LittleEndian()`, `ReadUint16LittleEndian()`, `ReadUint32LittleEndian()`, `ReadInt32LittleEndian()`
### Angle Conversion
- Angles in sensor units need to be divided by `DerivedValues.AngleResolution` (4194304.0) to get radians
- Example: `angleRad = sensorAngle / DerivedValues.AngleResolution`
### Packet Structure
- UDP packets may be fragmented across multiple UDP packets
- Need UDPPacketMerger (similar to TcpPacketMerger) to merge fragmented packets
- ParseDataHeader is always at offset 0
- Other blocks start at offsets specified in DataHeader
### Error Handling
- Each parser should check if the block is enabled (offset != 0 && size != 0)
- Return empty structure if block is not enabled
- Validate buffer size before parsing
## Usage in ILidar
The `ScanDataEventArgs` now includes:
- `RawScanData`: Raw bytes from UDP packet
- `ParsedScanData`: Parsed `UdpScanData` structure (if parsing succeeded)
This allows consumers to either:
1. Use parsed data directly (recommended)
2. Parse raw data themselves if needed
## Next Steps
1. Implement remaining parser classes
2. Create UDPPacketMerger for handling fragmented UDP packets
3. Integrate parsers into SickLidarDriver
4. Add UDP client support (if not already present)

View File

@@ -0,0 +1,247 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for application data block from UDP packets
/// Contains application inputs and outputs (local inputs/outputs, velocities, monitoring cases, etc.)
/// Thread-safe implementation
/// </summary>
public sealed class ParseApplicationData
{
/// <summary>
/// Parses the application data block from a UDP sequence
/// </summary>
/// <param name="buffer">Packet buffer containing the data</param>
/// <param name="header">Parsed data header (must contain valid block offset/size)</param>
public ApplicationData ParseUdpSequence(PacketBuffer buffer, DataHeader header)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (header == null)
throw new ArgumentNullException(nameof(header));
// Check if application data block is enabled
if (!CheckIfApplicationDataIsPublished(header))
{
return new ApplicationData
{
IsEmpty = true
};
}
// Check if header is valid
if (header.IsEmpty)
{
return new ApplicationData
{
IsEmpty = true
};
}
var bufferData = buffer.GetBuffer();
var offset = header.ApplicationDataBlockOffset;
// Validate buffer size (at least 260 bytes for full application data block)
if (offset + 260 > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to contain application data block (offset: {offset}, buffer size: {bufferData.Length})",
nameof(buffer));
}
var span = bufferData.Span.Slice(offset);
// Parse application inputs (offsets 0-74)
var inputs = ParseApplicationInputs(span);
// Parse application outputs (offsets 140-259)
var outputs = ParseApplicationOutputs(span);
return new ApplicationData
{
Inputs = inputs,
Outputs = outputs,
IsEmpty = false
};
}
/// <summary>
/// Parses application inputs from the data block
/// Inputs span from offset 0 to approximately offset 74
/// </summary>
private static ApplicationInputs ParseApplicationInputs(ReadOnlySpan<byte> span)
{
// Parse unsafe inputs (offsets 0-7)
var unsafeInputsSources = ParseBitVector32(span, 0);
var unsafeInputsFlags = ParseBitVector32(span, 4);
// Parse monitoring case inputs (offsets 12-51)
var monitoringCases = new List<ushort>(20);
for (int i = 0; i < 20; i++)
{
monitoringCases.Add(ReadWriteHelper.ReadUint16LittleEndian(span, 12 + i * 2));
}
// Parse monitoring case flags (offset 52)
var monitoringCaseFlags = ParseBitVector20(span, 52);
// Parse linear velocity inputs (offsets 56-60)
var velocity0 = (short)ReadWriteHelper.ReadUint16LittleEndian(span, 56);
var velocity1 = (short)ReadWriteHelper.ReadUint16LittleEndian(span, 58);
// Parse linear velocity flags (offset 60)
var velocityFlags = ReadWriteHelper.ReadUint8LittleEndian(span, 60);
var isVelocity0Valid = (velocityFlags & 0x01) != 0;
var isVelocity1Valid = (velocityFlags & 0x02) != 0;
// Bits 2,3 reserved
var isVelocity0TransmittedSafely = (velocityFlags & 0x10) != 0;
var isVelocity1TransmittedSafely = (velocityFlags & 0x20) != 0;
// Parse sleep mode input (offset 74)
var sleepModeInput = (sbyte)ReadWriteHelper.ReadUint8LittleEndian(span, 74);
return new ApplicationInputs
{
UnsafeInputsInputSources = unsafeInputsSources,
UnsafeInputsFlags = unsafeInputsFlags,
MonitoringCases = monitoringCases,
MonitoringCaseFlags = monitoringCaseFlags,
Velocity0 = velocity0,
Velocity1 = velocity1,
IsVelocity0Valid = isVelocity0Valid,
IsVelocity1Valid = isVelocity1Valid,
IsVelocity0TransmittedSafely = isVelocity0TransmittedSafely,
IsVelocity1TransmittedSafely = isVelocity1TransmittedSafely,
SleepModeInput = sleepModeInput
};
}
/// <summary>
/// Parses application outputs from the data block
/// Outputs span from offset 140 to approximately offset 259
/// </summary>
private static ApplicationOutputs ParseApplicationOutputs(ReadOnlySpan<byte> span)
{
// Parse evaluation paths outputs (offsets 140-151)
var evalOut = ParseBitVector20(span, 140);
var evalOutIsSafe = ParseBitVector20(span, 144);
var evalOutIsValid = ParseBitVector20(span, 148);
// Parse monitoring case outputs (offsets 152-195)
var outputMonitoringCases = new List<ushort>(20);
for (int i = 0; i < 20; i++)
{
outputMonitoringCases.Add(ReadWriteHelper.ReadUint16LittleEndian(span, 152 + i * 2));
}
var outputMonitoringCaseFlags = ParseBitVector20(span, 192);
// Parse sleep mode output (offset 193)
var sleepModeOutput = (sbyte)ReadWriteHelper.ReadUint8LittleEndian(span, 193);
// Parse error flags (offset 194)
var errorFlags = ReadWriteHelper.ReadUint8LittleEndian(span, 194);
var hostErrorFlagContaminationWarning = (errorFlags & 0x01) != 0;
var hostErrorFlagContaminationError = (errorFlags & 0x02) != 0;
var hostErrorFlagManipulationError = (errorFlags & 0x04) != 0;
var hostErrorFlagGlare = (errorFlags & 0x08) != 0;
var hostErrorFlagReferenceContourIntruded = (errorFlags & 0x10) != 0;
var hostErrorFlagCriticalError = (errorFlags & 0x20) != 0;
// Parse linear velocity outputs (offsets 200-204)
var outputVelocity0 = (short)ReadWriteHelper.ReadUint16LittleEndian(span, 200);
var outputVelocity1 = (short)ReadWriteHelper.ReadUint16LittleEndian(span, 202);
var outputVelocityFlags = ReadWriteHelper.ReadUint8LittleEndian(span, 204);
var isOutputVelocity0Valid = (outputVelocityFlags & 0x01) != 0;
var isOutputVelocity1Valid = (outputVelocityFlags & 0x02) != 0;
// Bits 2,3 reserved
var isOutputVelocity0TransmittedSafely = (outputVelocityFlags & 0x10) != 0;
var isOutputVelocity1TransmittedSafely = (outputVelocityFlags & 0x20) != 0;
// Bits 6,7 reserved
// Parse resulting velocities (offsets 208-247)
var resultingVelocities = new List<short>(20);
for (int i = 0; i < 20; i++)
{
resultingVelocities.Add(ReadWriteHelper.ReadInt16LittleEndian(span, 208 + i * 2));
}
var resultingVelocityFlags = ParseBitVector20(span, 248);
// Parse output flags (offset 259)
var outputFlags = ReadWriteHelper.ReadUint8LittleEndian(span, 259);
var flagsSleepModeOutputIsValid = (outputFlags & 0x01) != 0;
var flagsHostErrorFlagsAreValid = (outputFlags & 0x02) != 0;
return new ApplicationOutputs
{
EvalOut = evalOut,
EvalOutIsSafe = evalOutIsSafe,
EvalOutIsValid = evalOutIsValid,
MonitoringCases = outputMonitoringCases,
MonitoringCaseFlags = outputMonitoringCaseFlags,
SleepModeOutput = sleepModeOutput,
HostErrorFlagContaminationWarning = hostErrorFlagContaminationWarning,
HostErrorFlagContaminationError = hostErrorFlagContaminationError,
HostErrorFlagManipulationError = hostErrorFlagManipulationError,
HostErrorFlagGlare = hostErrorFlagGlare,
HostErrorFlagReferenceContourIntruded = hostErrorFlagReferenceContourIntruded,
HostErrorFlagCriticalError = hostErrorFlagCriticalError,
Velocity0 = outputVelocity0,
Velocity1 = outputVelocity1,
IsVelocity0Valid = isOutputVelocity0Valid,
IsVelocity1Valid = isOutputVelocity1Valid,
IsVelocity0TransmittedSafely = isOutputVelocity0TransmittedSafely,
IsVelocity1TransmittedSafely = isOutputVelocity1TransmittedSafely,
ResultingVelocity = resultingVelocities,
ResultingVelocityIsValid = resultingVelocityFlags,
FlagsSleepModeOutputIsValid = flagsSleepModeOutputIsValid,
FlagsHostErrorFlagsAreValid = flagsHostErrorFlagsAreValid
};
}
/// <summary>
/// Parses a 32-bit bit vector (32 boolean flags) from a uint32 value
/// </summary>
private static IReadOnlyList<bool> ParseBitVector32(ReadOnlySpan<byte> span, int offset)
{
var value = ReadWriteHelper.ReadUint32LittleEndian(span, offset);
var flags = new List<bool>(32);
for (int i = 0; i < 32; i++)
{
flags.Add((value & (0x01U << i)) != 0);
}
return flags;
}
/// <summary>
/// Parses a 20-bit bit vector (20 boolean flags) from a uint32 value
/// </summary>
private static IReadOnlyList<bool> ParseBitVector20(ReadOnlySpan<byte> span, int offset)
{
var value = ReadWriteHelper.ReadUint32LittleEndian(span, offset);
var flags = new List<bool>(20);
for (int i = 0; i < 20; i++)
{
flags.Add((value & (0x01U << i)) != 0);
}
return flags;
}
/// <summary>
/// Checks if application data block is published (enabled)
/// </summary>
private static bool CheckIfApplicationDataIsPublished(DataHeader header)
{
return !(header.ApplicationDataBlockOffset == 0 && header.ApplicationDataBlockSize == 0);
}
}

View File

@@ -0,0 +1,89 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for ApplicationName response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseApplicationName
{
/// <summary>
/// Parses the application name from a TCP sequence (COLA2 response)
/// Matches: ParseApplicationNameData::parseTCPSequence in C++
/// </summary>
public ApplicationName ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new ApplicationName
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
NameLength = ReadNameLength(span),
Name = ReadApplicationName(span)
};
}
/// <summary>
/// Matches: ParseApplicationNameData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseApplicationNameData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseApplicationNameData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseApplicationNameData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
/// <summary>
/// Matches: ParseApplicationNameData::readNameLength in C++
/// </summary>
private uint ReadNameLength(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 4);
}
/// <summary>
/// Matches: ParseApplicationNameData::readApplicationName in C++
/// </summary>
private string ReadApplicationName(ReadOnlySpan<byte> span)
{
uint nameLength = ReadWriteHelper.ReadUint32LittleEndian(span, 4);
var nameBuilder = new StringBuilder((int)nameLength);
for (uint i = 0; i < nameLength; i++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 8 + (int)i);
nameBuilder.Append((char)ch);
}
return nameBuilder.ToString();
}
}

View File

@@ -0,0 +1,109 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for ConfigMetadata response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseConfigMetadata
{
/// <summary>
/// Parses the config metadata from a TCP sequence (COLA2 response)
/// Matches: ParseConfigMetadata::parseTCPSequence in C++
/// </summary>
public ConfigMetadata ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new ConfigMetadata
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
ModificationTimeDate = ReadModificationTimeDate(span),
ModificationTimeTime = ReadModificationTimeTime(span),
TransferTimeDate = ReadTransferTimeDate(span),
TransferTimeTime = ReadTransferTimeTime(span),
AppChecksum = ReadAppChecksum(span),
OverallChecksum = ReadOverallChecksum(span),
IntegrityHash = ReadIntegrityHash(span)
};
}
/// <summary>
/// Matches: ParseConfigMetadata::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseConfigMetadata::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseConfigMetadata::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseConfigMetadata::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
private ushort ReadModificationTimeDate(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 4);
}
private uint ReadModificationTimeTime(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 8);
}
private ushort ReadTransferTimeDate(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 12);
}
private uint ReadTransferTimeTime(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 16);
}
private uint ReadAppChecksum(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32BigEndian(span, 36);
}
private uint ReadOverallChecksum(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32BigEndian(span, 52);
}
private IReadOnlyList<uint> ReadIntegrityHash(ReadOnlySpan<byte> span)
{
var result = new List<uint>(4);
for (int i = 0; i < 4; i++)
{
uint value = ReadWriteHelper.ReadUint32LittleEndian(span, 68 + (i * 4));
result.Add(value);
}
return result;
}
}

View File

@@ -0,0 +1,108 @@
using Sick.SafetyScanners.DataStructures;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Main parser that coordinates parsing of all data blocks from UDP packets
/// Thread-safe implementation
/// </summary>
public sealed class ParseData
{
private readonly ParseDataHeader _headerParser;
private readonly ParseDerivedValues _derivedValuesParser;
private readonly ParseMeasurementData _measurementDataParser;
private readonly ParseGeneralSystemState _generalSystemStateParser;
private readonly ParseIntrusionData _intrusionDataParser;
private readonly ParseApplicationData _applicationDataParser;
/// <summary>
/// Creates a new ParseData instance
/// </summary>
public ParseData()
{
_headerParser = new ParseDataHeader();
_derivedValuesParser = new ParseDerivedValues();
_measurementDataParser = new ParseMeasurementData();
_generalSystemStateParser = new ParseGeneralSystemState();
_intrusionDataParser = new ParseIntrusionData();
_applicationDataParser = new ParseApplicationData();
}
/// <summary>
/// Parses the complete UDP sequence into UdpScanData
/// </summary>
public UdpScanData ParseUdpSequence(PacketBuffer buffer)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
// Parse header first (required for all other parsers)
var header = _headerParser.ParseUdpSequence(buffer);
// Validate packet size before parsing other blocks
ValidatePacketSize(buffer, header);
// Parse all data blocks in order
// 1. DerivedValues (needed for MeasurementData and IntrusionData)
var derivedValues = _derivedValuesParser.ParseUdpSequence(buffer, header);
// 2. MeasurementData (needs DerivedValues for angle calculation)
var measurementData = _measurementDataParser.ParseUdpSequence(buffer, header, derivedValues);
// 3. GeneralSystemState (independent)
var generalSystemState = _generalSystemStateParser.ParseUdpSequence(buffer, header);
// 4. IntrusionData (needs DerivedValues for number of scan points)
var intrusionData = _intrusionDataParser.ParseUdpSequence(buffer, header, derivedValues);
// 5. ApplicationData (independent)
var applicationData = _applicationDataParser.ParseUdpSequence(buffer, header);
return new UdpScanData
{
Header = header,
DerivedValues = derivedValues.IsEmpty ? null : derivedValues,
MeasurementData = measurementData.IsEmpty ? null : measurementData,
GeneralSystemState = generalSystemState.IsEmpty ? null : generalSystemState,
IntrusionData = intrusionData.IsEmpty ? null : intrusionData,
ApplicationData = applicationData.IsEmpty ? null : applicationData
};
}
/// <summary>
/// Parses the complete TCP sequence (from COLA2 command response) into UdpScanData
/// Note: TCP and UDP use the same data structure format, only the transport differs
/// </summary>
public UdpScanData ParseTcpSequence(PacketBuffer buffer)
{
// TCP sequence uses the same format as UDP for the data payload
return ParseUdpSequence(buffer);
}
/// <summary>
/// Validates that the packet buffer contains enough data for all enabled blocks
/// </summary>
private static void ValidatePacketSize(PacketBuffer buffer, DataHeader header)
{
if (header.IsEmpty)
return;
// Calculate expected minimum size
var expectedSize = (uint)(
header.DerivedValuesBlockSize +
header.MeasurementDataBlockSize +
header.GeneralSystemStateBlockSize +
header.IntrusionDataBlockSize +
header.ApplicationDataBlockSize);
var actualSize = (uint)buffer.GetBuffer().Length;
if (actualSize < expectedSize)
{
// Log warning would go here in production
// For now, we'll let individual parsers handle missing data gracefully
// by checking block offsets and sizes
}
}
}

View File

@@ -0,0 +1,53 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for the data header from UDP packets
/// Thread-safe implementation
/// </summary>
public sealed class ParseDataHeader
{
/// <summary>
/// Parses the data header from a UDP sequence
/// </summary>
public DataHeader ParseUdpSequence(PacketBuffer buffer)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
var bufferData = buffer.GetBuffer();
if (bufferData.Length < 52) // Minimum header size
throw new ArgumentException($"Buffer too small (got {bufferData.Length}, expected at least 52)", nameof(buffer));
var span = bufferData.Span;
return new DataHeader
{
VersionIndicator = ReadWriteHelper.ReadUint8LittleEndian(span, 0),
VersionMajor = ReadWriteHelper.ReadUint8LittleEndian(span, 1),
VersionMinor = ReadWriteHelper.ReadUint8LittleEndian(span, 2),
VersionRelease = ReadWriteHelper.ReadUint8LittleEndian(span, 3),
SerialNumberOfDevice = ReadWriteHelper.ReadUint32LittleEndian(span, 4),
SerialNumberOfSystemPlug = ReadWriteHelper.ReadUint32LittleEndian(span, 8),
ChannelNumber = ReadWriteHelper.ReadUint8LittleEndian(span, 12),
// Offset 13-15 reserved
SequenceNumber = ReadWriteHelper.ReadUint32LittleEndian(span, 16),
ScanNumber = ReadWriteHelper.ReadUint32LittleEndian(span, 20),
TimestampDate = ReadWriteHelper.ReadUint16LittleEndian(span, 24),
TimestampTime = ReadWriteHelper.ReadUint32LittleEndian(span, 28),
GeneralSystemStateBlockOffset = ReadWriteHelper.ReadUint16LittleEndian(span, 32),
GeneralSystemStateBlockSize = ReadWriteHelper.ReadUint16LittleEndian(span, 34),
DerivedValuesBlockOffset = ReadWriteHelper.ReadUint16LittleEndian(span, 36),
DerivedValuesBlockSize = ReadWriteHelper.ReadUint16LittleEndian(span, 38),
MeasurementDataBlockOffset = ReadWriteHelper.ReadUint16LittleEndian(span, 40),
MeasurementDataBlockSize = ReadWriteHelper.ReadUint16LittleEndian(span, 42),
IntrusionDataBlockOffset = ReadWriteHelper.ReadUint16LittleEndian(span, 44),
IntrusionDataBlockSize = ReadWriteHelper.ReadUint16LittleEndian(span, 46),
ApplicationDataBlockOffset = ReadWriteHelper.ReadUint16LittleEndian(span, 48),
ApplicationDataBlockSize = ReadWriteHelper.ReadUint16LittleEndian(span, 50),
IsEmpty = false
};
}
}

View File

@@ -0,0 +1,38 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for the datagram header from UDP packets
/// Thread-safe implementation
/// </summary>
public sealed class ParseDatagramHeader
{
/// <summary>
/// Parses the UDP sequence to get the identification and offset for the datagram header
/// </summary>
public DatagramHeader ParseUdpSequence(PacketBuffer buffer)
{
ArgumentNullException.ThrowIfNull(buffer);
var bufferData = buffer.GetBuffer();
if (bufferData.Length < DatagramHeader.HeaderSize)
throw new ArgumentException($"Buffer too small (got {bufferData.Length}, expected at least {DatagramHeader.HeaderSize})",
nameof(buffer));
var span = bufferData.Span;
return new DatagramHeader
{
DatagramMarker = ReadWriteHelper.ReadUint32BigEndian(span, 0),
Protocol = ReadWriteHelper.ReadUint16BigEndian(span, 4),
MajorVersion = ReadWriteHelper.ReadUint8LittleEndian(span, 6),
MinorVersion = ReadWriteHelper.ReadUint8LittleEndian(span, 7),
TotalLength = ReadWriteHelper.ReadUint32LittleEndian(span, 8),
Identification = ReadWriteHelper.ReadUint32LittleEndian(span, 12),
FragmentOffset = ReadWriteHelper.ReadUint32LittleEndian(span, 16)
};
}
}

View File

@@ -0,0 +1,114 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for derived values block from UDP packets
/// Contains configuration of data output: multiplication factor, number of beams, scan time, angles, resolution
/// Thread-safe implementation
/// </summary>
public sealed class ParseDerivedValues
{
/// <summary>
/// Parses the derived values block from a UDP sequence
/// </summary>
/// <param name="buffer">Packet buffer containing the data</param>
/// <param name="header">Parsed data header (must contain valid block offset/size)</param>
public DerivedValues ParseUdpSequence(PacketBuffer buffer, DataHeader header)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (header == null)
throw new ArgumentNullException(nameof(header));
// Check if derived values block is enabled
if (!CheckIfDerivedValuesIsPublished(header))
{
return new DerivedValues
{
IsEmpty = true
};
}
// Check if header is valid
if (header.IsEmpty)
{
return new DerivedValues
{
IsEmpty = true
};
}
var bufferData = buffer.GetBuffer();
var offset = header.DerivedValuesBlockOffset;
// Validate buffer size
if (offset + 20 > bufferData.Length) // Derived values block is at least 20 bytes
{
throw new ArgumentException(
$"Buffer too small to contain derived values block (offset: {offset}, buffer size: {bufferData.Length})",
nameof(buffer));
}
var span = bufferData.Span.Slice(offset);
// Parse all fields from the derived values block
// Format (all little endian):
// Offset 0: Multiplication factor (uint16)
// Offset 2: Number of beams (uint16)
// Offset 4: Scan time (uint16, milliseconds)
// Offset 6: Reserved (2 bytes)
// Offset 8: Start angle (int32, sensor units)
// Offset 12: Angular beam resolution (int32, sensor units)
// Offset 16: Interbeam period (uint32, microseconds)
var multiplicationFactor = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var numberOfBeams = ReadWriteHelper.ReadUint16LittleEndian(span, 2);
var scanTime = ReadWriteHelper.ReadUint16LittleEndian(span, 4);
var startAngleSensorUnits = ReadWriteHelper.ReadInt32LittleEndian(span, 8);
var angularBeamResolutionSensorUnits = ReadWriteHelper.ReadInt32LittleEndian(span, 12);
var interbeamPeriod = ReadWriteHelper.ReadUint32LittleEndian(span, 16);
// Convert angles from sensor units to radians
// According to COLA2 documentation: "This value, divided by 4194304, equals the actual start angle"
// According to C++ reference: value / 4194304.0 gives degrees (see setDerivedAngularBeamResolutionDegrees)
// The C++ reference's setDerivedAngularBeamResolution divides by 4194304.0, and setDerivedAngularBeamResolutionDegrees
// sets degrees directly, indicating that dividing by 4194304.0 gives degrees.
// However, looking at ParseMeasurementData which uses these values to calculate scan point angles,
// and the fact that 4194304.0 units represent a full circle (360° = 2π radians),
// it appears the C++ reference stores values as "full circle units" (1.0 = 360° = 2π radians).
// So: sensorUnits / 4194304.0 = full circle units, then * 2π = radians
// OR: sensorUnits / 4194304.0 = degrees / 360.0, so * 360 = degrees, then * π/180 = radians
// Since user reports resolution shows 60° which is too large, let's check the actual conversion.
// Based on user feedback that resolution shows 60° (not typical ~0.1°), the current formula
// (multiplying by 2π) might be converting full circle units incorrectly.
// Let's try: sensorUnits / 4194304.0 already gives degrees (as per C++ setDerivedAngularBeamResolutionDegrees pattern)
var startAngleDeg = (startAngleSensorUnits / DerivedValues.AngleResolution);
var angularBeamResolutionDeg = (angularBeamResolutionSensorUnits / DerivedValues.AngleResolution);
// Convert degrees to radians
var startAngleRad = (startAngleDeg * Math.PI / 180.0);
var angularBeamResolutionRad = (angularBeamResolutionDeg * Math.PI / 180.0);
return new DerivedValues
{
MultiplicationFactor = multiplicationFactor,
NumberOfBeams = numberOfBeams,
ScanTime = scanTime,
StartAngle = startAngleRad,
AngularBeamResolution = angularBeamResolutionRad,
InterbeamPeriod = interbeamPeriod,
IsEmpty = false
};
}
/// <summary>
/// Checks if derived values block is published (enabled)
/// </summary>
private static bool CheckIfDerivedValuesIsPublished(DataHeader header)
{
return !(header.DerivedValuesBlockOffset == 0 && header.DerivedValuesBlockSize == 0);
}
}

View File

@@ -0,0 +1,44 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for DeviceName response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseDeviceName
{
/// <summary>
/// Parses the device name from a TCP sequence (COLA2 response)
/// Matches: ParseDeviceName::parseTCPSequence in C++
/// </summary>
public DeviceName ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new DeviceName
{
Name = ReadDeviceName(span)
};
}
/// <summary>
/// Reads device name from buffer
/// Matches: ParseDeviceName::readDeviceName in C++
/// </summary>
private string ReadDeviceName(ReadOnlySpan<byte> span)
{
ushort stringLength = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var nameBuilder = new StringBuilder(stringLength);
for (ushort i = 0; i < stringLength; i++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 2 + i);
nameBuilder.Append((char)ch);
}
return nameBuilder.ToString();
}
}

View File

@@ -0,0 +1,33 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for DeviceStatus response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseDeviceStatus
{
/// <summary>
/// Parses the device status from a TCP sequence (COLA2 response)
/// Matches: ParseDeviceStatusData::parseTCPSequence in C++
/// </summary>
public DeviceStatus ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new DeviceStatus
{
Status = (SopasDeviceStatus)ReadDeviceStatus(span)
};
}
/// <summary>
/// Matches: ParseDeviceStatusData::readDeviceStatus in C++
/// </summary>
private byte ReadDeviceStatus(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 0);
}
}

View File

@@ -0,0 +1,51 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for FieldGeometryData response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseFieldGeometryData
{
private const double StartAngleDegrees = -47.5; // Defined start angle in degrees in SICK coordinates
/// <summary>
/// Parses the field geometry data from a TCP sequence (COLA2 response)
/// Matches: ParseFieldGeometryData::parseTCPSequence in C++
/// </summary>
public FieldData ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
uint arrayLength = ReadArrayLength(span);
var geometryDistance = new List<ushort>((int)arrayLength);
for (uint i = 0; i < arrayLength; i++)
{
geometryDistance.Add(ReadArrayElement(span, i));
}
// Values are persistent for scanners
double res = (275.0 / arrayLength);
return new FieldData
{
IsValid = true,
BeamDistances = geometryDistance,
StartAngle = (StartAngleDegrees * Math.PI / 180.0),
AngularBeamResolution = (res * Math.PI / 180.0)
};
}
private uint ReadArrayLength(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 4);
}
private ushort ReadArrayElement(ReadOnlySpan<byte> span, uint elemNumber)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 8 + (int)(elemNumber * 2));
}
}

View File

@@ -0,0 +1,176 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for FieldHeaderData response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseFieldHeaderData
{
/// <summary>
/// Parses the field header data from a TCP sequence (COLA2 response)
/// Matches: ParseFieldHeaderData::parseTCPSequence in C++
/// </summary>
public FieldData ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
bool valid = IsValid(span);
if (!valid)
{
return new FieldData
{
IsValid = false
};
}
SetFieldType(span, out bool isWarningField, out bool isProtectiveField);
ushort setIndex = ReadSetIndex(span);
uint nameLength = ReadNameLength(span);
return new FieldData
{
IsValid = true,
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
IsDefined = ReadIsDefined(span),
EvalMethod = ReadEvalMethod(span),
MultiSampling = ReadMultiSampling(span),
ObjectResolution = ReadObjectResolution(span),
FieldSetIndex = setIndex,
NameLength = nameLength,
FieldName = ReadFieldName(span, nameLength),
IsWarningField = isWarningField,
IsProtectiveField = isProtectiveField
};
}
/// <summary>
/// Matches: ParseFieldHeaderData::isValid in C++
/// </summary>
private bool IsValid(ReadOnlySpan<byte> span)
{
byte byteValue = ReadWriteHelper.ReadUint8(span, 0);
return byteValue == 'R' || byteValue == 'Y';
}
/// <summary>
/// Matches: ParseFieldHeaderData::setFieldType in C++
/// </summary>
private void SetFieldType(ReadOnlySpan<byte> span, out bool isWarningField, out bool isProtectiveField)
{
byte fieldType = ReadEvalMethod(span);
isWarningField = false;
isProtectiveField = false;
if (fieldType == 4 || fieldType == 14)
{
isProtectiveField = true;
}
else if (fieldType == 5 || fieldType == 15)
{
isWarningField = true;
}
}
/// <summary>
/// Matches: ParseFieldHeaderData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseFieldHeaderData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readIsDefined in C++
/// </summary>
private bool ReadIsDefined(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 72) != 0;
}
/// <summary>
/// Matches: ParseFieldHeaderData::readEvalMethod in C++
/// </summary>
private byte ReadEvalMethod(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 73);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readMultiSampling in C++
/// </summary>
private ushort ReadMultiSampling(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 74);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readObjectResolution in C++
/// </summary>
private ushort ReadObjectResolution(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 78);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readSetIndex in C++
/// </summary>
private ushort ReadSetIndex(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 82);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readNameLength in C++
/// </summary>
private uint ReadNameLength(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 84);
}
/// <summary>
/// Matches: ParseFieldHeaderData::readFieldName in C++
/// </summary>
private string ReadFieldName(ReadOnlySpan<byte> span, uint nameLength)
{
var nameBuilder = new StringBuilder((int)nameLength);
for (uint i = 0; i < nameLength; i++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 88 + (int)i);
nameBuilder.Append((char)ch);
}
return nameBuilder.ToString();
}
}

View File

@@ -0,0 +1,93 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for FieldSetsData response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseFieldSetsData
{
/// <summary>
/// Parses the field sets data from a TCP sequence (COLA2 response)
/// Matches: ParseFieldSetsData::parseTCPSequence in C++
/// </summary>
public FieldSets ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
uint arrayLength = ReadArrayLength(span);
var nameLengths = new List<uint>((int)arrayLength);
var fieldNames = new List<string>((int)arrayLength);
var isDefined = new List<bool>((int)arrayLength);
for (uint i = 0; i < arrayLength; i++)
{
uint nameLength = ReadWriteHelper.ReadUint32LittleEndian(span, 8 + (int)(i * 104));
nameLengths.Add(nameLength);
var nameBuilder = new StringBuilder((int)nameLength);
for (uint j = 0; j < nameLength; j++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 12 + (int)(i * 104) + (int)j);
nameBuilder.Append((char)ch);
}
fieldNames.Add(nameBuilder.ToString());
byte byteValue = ReadWriteHelper.ReadUint8(span, 44 + (int)(i * 104));
isDefined.Add((byteValue & (0x01 << 0)) != 0);
}
return new FieldSets
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
NameLengths = nameLengths,
FieldNames = fieldNames,
IsDefined = isDefined
};
}
/// <summary>
/// Matches: ParseFieldSetsData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseFieldSetsData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseFieldSetsData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseFieldSetsData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
private uint ReadArrayLength(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 4);
}
}

View File

@@ -0,0 +1,44 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for FirmwareVersion response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseFirmwareVersion
{
/// <summary>
/// Parses the firmware version from a TCP sequence (COLA2 response)
/// Matches: ParseFirmwareVersion::parseTCPSequence in C++
/// </summary>
public static FirmwareVersion ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new FirmwareVersion
{
Version = ReadFirmwareVersion(span)
};
}
/// <summary>
/// Reads firmware version from buffer
/// Matches: ParseFirmwareVersion::readFirmwareVersion in C++
/// </summary>
private static string ReadFirmwareVersion(ReadOnlySpan<byte> span)
{
ushort stringLength = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var versionBuilder = new StringBuilder(stringLength);
for (ushort i = 0; i < stringLength; i++)
{
ushort ch = ReadWriteHelper.ReadUint8(span, 2 + i);
versionBuilder.Append((char)ch);
}
return versionBuilder.ToString();
}
}

View File

@@ -0,0 +1,142 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for general system state block from UDP packets
/// Contains device status, cut-off paths, monitoring cases, and errors
/// Thread-safe implementation
/// </summary>
public sealed class ParseGeneralSystemState
{
/// <summary>
/// Parses the general system state block from a UDP sequence
/// </summary>
/// <param name="buffer">Packet buffer containing the data</param>
/// <param name="header">Parsed data header (must contain valid block offset/size)</param>
public GeneralSystemState ParseUdpSequence(PacketBuffer buffer, DataHeader header)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (header == null)
throw new ArgumentNullException(nameof(header));
// Check if general system state block is enabled
if (!CheckIfGeneralSystemStateIsPublished(header))
{
return new GeneralSystemState
{
IsEmpty = true
};
}
// Check if header is valid
if (header.IsEmpty)
{
return new GeneralSystemState
{
IsEmpty = true
};
}
var bufferData = buffer.GetBuffer();
var offset = header.GeneralSystemStateBlockOffset;
// Validate buffer size (at least 16 bytes for all fields)
if (offset + 16 > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to contain general system state block (offset: {offset}, buffer size: {bufferData.Length})",
nameof(buffer));
}
var span = bufferData.Span.Slice(offset);
// Parse status bits (offset 0)
var statusByte = ReadWriteHelper.ReadUint8LittleEndian(span, 0);
var isRunModeActive = (statusByte & 0x01) != 0;
var isStandbyModeActive = (statusByte & 0x02) != 0;
var hasContaminationWarning = (statusByte & 0x04) != 0;
var hasContaminationError = (statusByte & 0x08) != 0;
var referenceContourStatus = (statusByte & 0x10) != 0;
var manipulationStatus = (statusByte & 0x20) != 0;
// Bits 6 and 7 are reserved
// Parse safe cut-off paths (offsets 1-3)
var safeCutOffPaths = ParseCutOffPaths(span, 1);
// Parse non-safe cut-off paths (offsets 4-6)
var nonSafeCutOffPaths = ParseCutOffPaths(span, 4);
// Parse reset required cut-off paths (offsets 7-9)
var resetRequiredCutOffPaths = ParseCutOffPaths(span, 7);
// Parse monitoring cases (offsets 10-13)
var currentMonitoringCaseNoTable1 = ReadWriteHelper.ReadUint8LittleEndian(span, 10);
var currentMonitoringCaseNoTable2 = ReadWriteHelper.ReadUint8LittleEndian(span, 11);
var currentMonitoringCaseNoTable3 = ReadWriteHelper.ReadUint8LittleEndian(span, 12);
var currentMonitoringCaseNoTable4 = ReadWriteHelper.ReadUint8LittleEndian(span, 13);
// Parse errors (offset 15)
var errorByte = ReadWriteHelper.ReadUint8LittleEndian(span, 15);
var hasApplicationError = (errorByte & 0x01) != 0;
var hasDeviceError = (errorByte & 0x02) != 0;
return new GeneralSystemState
{
IsRunModeActive = isRunModeActive,
IsStandbyModeActive = isStandbyModeActive,
HasContaminationWarning = hasContaminationWarning,
HasContaminationError = hasContaminationError,
ReferenceContourStatus = referenceContourStatus,
ManipulationStatus = manipulationStatus,
SafeCutOffPaths = safeCutOffPaths,
NonSafeCutOffPaths = nonSafeCutOffPaths,
ResetRequiredCutOffPaths = resetRequiredCutOffPaths,
CurrentMonitoringCaseNoTable1 = currentMonitoringCaseNoTable1,
CurrentMonitoringCaseNoTable2 = currentMonitoringCaseNoTable2,
CurrentMonitoringCaseNoTable3 = currentMonitoringCaseNoTable3,
CurrentMonitoringCaseNoTable4 = currentMonitoringCaseNoTable4,
HasApplicationError = hasApplicationError,
HasDeviceError = hasDeviceError,
IsEmpty = false
};
}
/// <summary>
/// Parses cut-off paths from 3 bytes (24 bits, but only 20 paths are used)
/// </summary>
private static IReadOnlyList<bool> ParseCutOffPaths(ReadOnlySpan<byte> span, int startOffset)
{
var paths = new List<bool>(20);
for (int i = 0; i < 3; i++)
{
var byteValue = ReadWriteHelper.ReadUint8LittleEndian(span, startOffset + i);
for (int j = 0; j < 8; j++)
{
// As long as there are only 20 instead of 24 cut-off paths
if (i == 2 && j > 3)
{
break;
}
paths.Add((byteValue & (0x01 << j)) != 0);
}
}
return paths;
}
/// <summary>
/// Checks if general system state block is published (enabled)
/// </summary>
private static bool CheckIfGeneralSystemStateIsPublished(DataHeader header)
{
return !(header.GeneralSystemStateBlockOffset == 0 && header.GeneralSystemStateBlockSize == 0);
}
}

View File

@@ -0,0 +1,161 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for intrusion data block from UDP packets
/// Contains intrusion data for 24 cut-off paths (field interruption)
/// Thread-safe implementation
/// </summary>
public sealed class ParseIntrusionData
{
private const int NumberOfIntrusionDatums = 24;
/// <summary>
/// Parses the intrusion data block from a UDP sequence
/// </summary>
/// <param name="buffer">Packet buffer containing the data</param>
/// <param name="header">Parsed data header (must contain valid block offset/size)</param>
/// <param name="derivedValues">Parsed derived values (required for number of scan points)</param>
public IntrusionData ParseUdpSequence(PacketBuffer buffer, DataHeader header, DerivedValues derivedValues)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (header == null)
throw new ArgumentNullException(nameof(header));
// Check if intrusion data block is enabled
if (!CheckIfIntrusionDataIsPublished(header))
{
return new IntrusionData
{
IsEmpty = true
};
}
// Check if header is valid
if (header.IsEmpty)
{
return new IntrusionData
{
IsEmpty = true
};
}
// Check if derived values are available (required for number of scan points)
if (derivedValues == null || derivedValues.IsEmpty)
{
return new IntrusionData
{
IsEmpty = true
};
}
var bufferData = buffer.GetBuffer();
var offset = header.IntrusionDataBlockOffset;
var numberOfScanPoints = derivedValues.NumberOfBeams;
// Validate buffer size (at least 4 bytes for first size field)
if (offset + 4 > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to contain intrusion data block header (offset: {offset}, buffer size: {bufferData.Length})",
nameof(buffer));
}
var span = bufferData.Span.Slice(offset);
var intrusionDatums = new List<IntrusionDatum>(NumberOfIntrusionDatums);
// Parse 24 intrusion datums
// Each datum consists of:
// - Size (4 bytes, uint32) - number of bytes in flags vector
// - Flags vector (variable size, 1 bit per scan point indicating intrusion)
int currentOffset = 0;
for (int i = 0; i < NumberOfIntrusionDatums; i++)
{
// Validate we have enough data for size field
if (offset + currentOffset + 4 > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to read intrusion datum {i} size (offset: {offset + currentOffset}, buffer size: {bufferData.Length})",
nameof(buffer));
}
// Read size (4 bytes, little endian uint32)
var sizeBytes = ReadWriteHelper.ReadUint32LittleEndian(span, currentOffset);
currentOffset += 4;
// Validate size is reasonable
if (sizeBytes > 10000) // Sanity check
{
throw new ArgumentException(
$"Invalid intrusion datum {i} size: {sizeBytes}",
nameof(buffer));
}
// Validate we have enough data for flags vector
if (offset + currentOffset + sizeBytes > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to read intrusion datum {i} flags (size: {sizeBytes}, available: {bufferData.Length - offset - currentOffset})",
nameof(buffer));
}
// Parse flags vector
// Each byte contains 8 flags (bits), one per scan point
var flags = new List<bool>((int)numberOfScanPoints);
uint numReadFlags = 0;
for (int byteIndex = 0; byteIndex < sizeBytes && numReadFlags < numberOfScanPoints; byteIndex++)
{
var byteValue = ReadWriteHelper.ReadUint8LittleEndian(span, currentOffset + byteIndex);
// Extract 8 bits from this byte
for (int bitIndex = 0; bitIndex < 8 && numReadFlags < numberOfScanPoints; bitIndex++, numReadFlags++)
{
flags.Add((byteValue & (0x01 << bitIndex)) != 0);
}
}
// Ensure we have exactly numberOfScanPoints flags (pad with false if needed)
while (flags.Count < numberOfScanPoints)
{
flags.Add(false);
}
// Truncate if we have more than numberOfScanPoints flags
if (flags.Count > numberOfScanPoints)
{
flags = flags.Take((int)numberOfScanPoints).ToList();
}
intrusionDatums.Add(new IntrusionDatum
{
Size = (int)sizeBytes,
Flags = flags
});
// Advance offset by size bytes
currentOffset += (int)sizeBytes;
}
return new IntrusionData
{
IntrusionDatums = intrusionDatums,
IsEmpty = false
};
}
/// <summary>
/// Checks if intrusion data block is published (enabled)
/// </summary>
private static bool CheckIfIntrusionDataIsPublished(DataHeader header)
{
return !(header.IntrusionDataBlockOffset == 0 && header.IntrusionDataBlockSize == 0);
}
}

View File

@@ -0,0 +1,177 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Net;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for MeasurementCurrentConfigData response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseMeasurementCurrentConfigData
{
private const double AngleResolution = 4194304.0; // Sensor units per radian
/// <summary>
/// Parses the current measurement config from a TCP sequence (COLA2 response)
/// Matches: ParseMeasurementCurrentConfigData::parseTCPSequence in C++
/// </summary>
public ConfigData ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
var features = ReadFeatures(span);
return new ConfigData
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
Enabled = ReadEnabled(span),
EInterfaceType = (InterfaceType)ReadInterfaceType(span),
HostIp = ReadHostIp(span),
HostUdpPort = ReadHostPort(span),
PublishingFrequency = ReadPublishingFreq(span),
StartAngle = ConvertToRadians(ReadStartAngle(span)),
EndAngle = ConvertToRadians(ReadEndAngle(span)),
Features = features,
GeneralSystemStateEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.GeneralSystemState),
DerivedSettingsEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.DerivedSettings),
MeasurementDataEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.MeasurementData),
IntrusionDataEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.IntrusionData),
ApplicationDataEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.ApplicationData),
DerivedMultiplicationFactor = ReadDerivedMultiplicationFactor(span),
DerivedNumberOfBeams = ReadDerivedNumBeams(span),
DerivedScanTime = ReadDerivedScanTime(span),
DerivedStartAngle = ConvertToRadians(ReadDerivedStartAngle(span)),
DerivedAngularBeamResolution = ConvertToRadians(ReadDerivedAngularBeamResolution(span)),
DerivedInterbeamPeriod = ReadDerivedInterbeamPeriod(span)
};
}
/// <summary>
/// Matches: ParseMeasurementCurrentConfigData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseMeasurementCurrentConfigData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseMeasurementCurrentConfigData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseMeasurementCurrentConfigData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
/// <summary>
/// Matches: ParseMeasurementCurrentConfigData::readEnabled in C++
/// </summary>
private bool ReadEnabled(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 4) != 0;
}
/// <summary>
/// Matches: ParseMeasurementCurrentConfigData::readInterfaceType in C++
/// </summary>
private byte ReadInterfaceType(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 5);
}
private string ReadHostIp(ReadOnlySpan<byte> span)
{
// IPAddress constructor expects bytes in network byte order (big endian)
// word is little endian: [b0, b1, b2, b3] represents IP b0.b1.b2.b3
// We need to extract bytes in order: b0, b1, b2, b3
var address = new IPAddress([span[11], span[10], span[9], span[8]]);
return address.ToString();
}
private ushort ReadHostPort(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 12);
}
private ushort ReadPublishingFreq(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 14);
}
private uint ReadStartAngle(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 16);
}
private uint ReadEndAngle(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 20);
}
private ushort ReadFeatures(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 24);
}
private ushort ReadDerivedMultiplicationFactor(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 28);
}
private ushort ReadDerivedNumBeams(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 30);
}
private ushort ReadDerivedScanTime(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 32);
}
private uint ReadDerivedStartAngle(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 36);
}
private uint ReadDerivedAngularBeamResolution(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 40);
}
private uint ReadDerivedInterbeamPeriod(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 44);
}
/// <summary>
/// Converts sensor units to radians
/// According to COLA2 documentation: "This value, divided by 4194304, equals the actual start angle"
/// According to C++ reference: value / 4194304.0 gives degrees (same as setDerivedAngularBeamResolutionDegrees)
/// So we convert: sensorUnits / 4194304.0 = degrees, then degrees * π/180 = radians
/// </summary>
private double ConvertToRadians(uint sensorUnits)
{
var degrees = (sensorUnits / AngleResolution);
return (degrees * Math.PI / 180.0);
}
}

View File

@@ -0,0 +1,136 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for measurement data block from UDP packets
/// Contains scan points with distance, reflectivity, and status flags
/// Thread-safe implementation
/// </summary>
public sealed class ParseMeasurementData
{
private const uint MaxExpectedBeams = 2751;
/// <summary>
/// Parses the measurement data block from a UDP sequence
/// </summary>
/// <param name="buffer">Packet buffer containing the data</param>
/// <param name="header">Parsed data header (must contain valid block offset/size)</param>
/// <param name="derivedValues">Parsed derived values (required for angle calculation)</param>
public MeasurementData ParseUdpSequence(PacketBuffer buffer, DataHeader header, DerivedValues derivedValues)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (header == null)
throw new ArgumentNullException(nameof(header));
// Check if measurement data block is enabled
if (!CheckIfMeasurementDataIsPublished(header))
{
return new MeasurementData(0, Array.Empty<ScanPoint>(), isEmpty: true);
}
// Check if header is valid
if (header.IsEmpty)
{
return new MeasurementData(0, Array.Empty<ScanPoint>(), isEmpty: true);
}
// Check if derived values are available (required for angle calculation)
if (derivedValues == null || derivedValues.IsEmpty)
{
return new MeasurementData(0, Array.Empty<ScanPoint>(), isEmpty: true);
}
var bufferData = buffer.GetBuffer();
var offset = header.MeasurementDataBlockOffset;
// Validate buffer size (at least 4 bytes for number of beams)
if (offset + 4 > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to contain measurement data block header (offset: {offset}, buffer size: {bufferData.Length})",
nameof(buffer));
}
var span = bufferData.Span.Slice(offset);
// Parse number of beams (first 4 bytes, little endian uint32)
var numberOfBeams = ReadWriteHelper.ReadUint32LittleEndian(span, 0);
// Validate number of beams (safety check)
if (numberOfBeams > MaxExpectedBeams)
{
// Log warning would go here in production
return new MeasurementData(0, Array.Empty<ScanPoint>(), isEmpty: true);
}
// Calculate required buffer size: 4 bytes (number of beams) + numberOfBeams * 4 bytes (per scan point)
var requiredSize = 4 + numberOfBeams * 4;
if (offset + requiredSize > bufferData.Length)
{
throw new ArgumentException(
$"Buffer too small to contain all measurement data (required: {requiredSize}, available: {bufferData.Length - offset})",
nameof(buffer));
}
// Parse scan points
var scanPoints = new List<ScanPoint>((int)numberOfBeams);
var currentAngle = derivedValues.StartAngle;
var angleDelta = derivedValues.AngularBeamResolution;
for (uint i = 0; i < numberOfBeams; i++)
{
// Each scan point is 4 bytes:
// Offset 4 + i*4: Distance (uint16, mm)
// Offset 6 + i*4: Reflectivity (uint8, 0-255)
// Offset 7 + i*4: Status flags (uint8)
// Bit 0: Valid
// Bit 1: Infinite
// Bit 2: Glare
// Bit 3: Reflector
// Bit 4: Contamination
// Bit 5: Contamination warning
var pointOffset = 4 + (int)(i * 4);
var distance = ReadWriteHelper.ReadUint16LittleEndian(span, pointOffset);
var reflectivity = ReadWriteHelper.ReadUint8LittleEndian(span, pointOffset + 2);
var status = ReadWriteHelper.ReadUint8LittleEndian(span, pointOffset + 3);
var isValid = (status & 0x01) != 0;
var isInfinite = (status & 0x02) != 0;
var hasGlare = (status & 0x04) != 0;
var isReflector = (status & 0x08) != 0;
var isContaminated = (status & 0x10) != 0;
var hasContaminationWarning = (status & 0x20) != 0;
scanPoints.Add(new ScanPoint(
angle: currentAngle,
distance: distance,
reflectivity: reflectivity,
isValid: isValid,
isInfinite: isInfinite,
hasGlare: hasGlare,
isReflector: isReflector,
isContaminated: isContaminated,
hasContaminationWarning: hasContaminationWarning
));
// Advance angle for next point
currentAngle += angleDelta;
}
return new MeasurementData(numberOfBeams, scanPoints, isEmpty: false);
}
/// <summary>
/// Checks if measurement data block is published (enabled)
/// </summary>
private static bool CheckIfMeasurementDataIsPublished(DataHeader header)
{
return !(header.MeasurementDataBlockOffset == 0 && header.MeasurementDataBlockSize == 0);
}
}

View File

@@ -0,0 +1,141 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Net;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for MeasurementPersistentConfigData response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseMeasurementPersistentConfigData
{
private const double AngleResolution = 4194304.0; // Sensor units per radian
/// <summary>
/// Parses the persistent measurement config from a TCP sequence (COLA2 response)
/// Matches: ParseMeasurementPersistentConfigData::parseTCPSequence in C++
/// </summary>
public ConfigData ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
var features = ReadFeatures(span);
return new ConfigData
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
Enabled = ReadEnabled(span),
EInterfaceType = (InterfaceType)ReadInterfaceType(span),
HostIp = ReadHostIp(span),
HostUdpPort = ReadHostPort(span),
PublishingFrequency = ReadPublishingFreq(span),
StartAngle = ConvertToRadians(ReadStartAngle(span)),
EndAngle = ConvertToRadians(ReadEndAngle(span)),
Features = features,
GeneralSystemStateEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.GeneralSystemState),
DerivedSettingsEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.DerivedSettings),
MeasurementDataEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.MeasurementData),
IntrusionDataEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.IntrusionData),
ApplicationDataEnabled = SensorDataFeatures.IsFlagSet(features, SensorDataFeatures.ApplicationData)
};
}
/// <summary>
/// Matches: ParseMeasurementPersistentConfigData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseMeasurementPersistentConfigData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseMeasurementPersistentConfigData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseMeasurementPersistentConfigData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
/// <summary>
/// Matches: ParseMeasurementPersistentConfigData::readEnabled in C++
/// </summary>
private bool ReadEnabled(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 4) != 0;
}
/// <summary>
/// Matches: ParseMeasurementPersistentConfigData::readInterfaceType in C++
/// </summary>
private byte ReadInterfaceType(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 5);
}
private string ReadHostIp(ReadOnlySpan<byte> span)
{
uint word = ReadWriteHelper.ReadUint32LittleEndian(span, 8);
// Convert uint32 (little endian from packet) to IP address bytes
// IPAddress constructor expects bytes in network byte order (big endian)
// word is little endian: [b0, b1, b2, b3] represents IP b0.b1.b2.b3
// We need to extract bytes in order: b0, b1, b2, b3
byte[] ipBytes = new byte[4];
ipBytes[0] = (byte)(word & 0xFF);
ipBytes[1] = (byte)((word >> 8) & 0xFF);
ipBytes[2] = (byte)((word >> 16) & 0xFF);
ipBytes[3] = (byte)((word >> 24) & 0xFF);
var address = new IPAddress(ipBytes);
return address.ToString();
}
private ushort ReadHostPort(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 12);
}
private ushort ReadPublishingFreq(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 14);
}
private uint ReadStartAngle(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 16);
}
private uint ReadEndAngle(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 20);
}
private ushort ReadFeatures(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 24);
}
private double ConvertToRadians(uint sensorUnits)
{
return (sensorUnits / AngleResolution);
}
}

View File

@@ -0,0 +1,81 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for MonitoringCaseData response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseMonitoringCaseData
{
/// <summary>
/// Parses the monitoring case data from a TCP sequence (COLA2 response)
/// Matches: ParseMonitoringCaseData::parseTCPSequence in C++
/// </summary>
public MonitoringCaseData ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
bool valid = IsValid(span);
if (!valid)
{
return new MonitoringCaseData
{
IsValid = false
};
}
var indices = new List<ushort>(8);
var fieldsValid = new List<bool>(8);
for (int i = 0; i < 8; i++)
{
indices.Add(ReadFieldIndex(span, i));
fieldsValid.Add(ReadFieldValid(span, i));
}
return new MonitoringCaseData
{
IsValid = true,
MonitoringCaseNumber = ReadMonitoringCaseNumber(span),
FieldIndices = indices,
FieldsValid = fieldsValid
};
}
/// <summary>
/// Matches: ParseMonitoringCaseData::isValid in C++
/// </summary>
private bool IsValid(ReadOnlySpan<byte> span)
{
byte byteValue = ReadWriteHelper.ReadUint8(span, 0);
return byteValue == 'R' || byteValue == 'Y';
}
/// <summary>
/// Matches: ParseMonitoringCaseData::readMonitoringCaseNumber in C++
/// </summary>
private ushort ReadMonitoringCaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 6);
}
/// <summary>
/// Matches: ParseMonitoringCaseData::readFieldIndex in C++
/// </summary>
private ushort ReadFieldIndex(ReadOnlySpan<byte> span, int index)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 158 + (index * 4));
}
/// <summary>
/// Matches: ParseMonitoringCaseData::readFieldValid in C++
/// </summary>
private bool ReadFieldValid(ReadOnlySpan<byte> span, int index)
{
byte byteValue = ReadWriteHelper.ReadUint8(span, 157 + (index * 4));
return (byteValue & (0x01 << 0)) != 0;
}
}

View File

@@ -0,0 +1,44 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for OrderNumber response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseOrderNumber
{
/// <summary>
/// Parses the order number from a TCP sequence (COLA2 response)
/// Matches: ParseOrderNumber::parseTCPSequence in C++
/// </summary>
public OrderNumber ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new OrderNumber
{
Number = ReadOrderNumber(span)
};
}
/// <summary>
/// Reads order number from buffer
/// Matches: ParseOrderNumber::readOrderNumber in C++
/// </summary>
private string ReadOrderNumber(ReadOnlySpan<byte> span)
{
ushort stringLength = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var numberBuilder = new StringBuilder(stringLength);
for (ushort i = 0; i < stringLength; i++)
{
ushort ch = ReadWriteHelper.ReadUint8(span, 2 + i);
numberBuilder.Append((char)ch);
}
return numberBuilder.ToString();
}
}

View File

@@ -0,0 +1,44 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for ProjectName response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseProjectName
{
/// <summary>
/// Parses the project name from a TCP sequence (COLA2 response)
/// Matches: ParseProjectName::parseTCPSequence in C++
/// </summary>
public ProjectName ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new ProjectName
{
Name = ReadProjectName(span)
};
}
/// <summary>
/// Reads project name from buffer
/// Matches: ParseProjectName::readProjectName in C++
/// </summary>
private string ReadProjectName(ReadOnlySpan<byte> span)
{
ushort stringLength = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var nameBuilder = new StringBuilder(stringLength);
for (ushort i = 0; i < stringLength; i++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 2 + i);
nameBuilder.Append((char)ch);
}
return nameBuilder.ToString();
}
}

View File

@@ -0,0 +1,42 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for RequiredUserAction response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseRequiredUserAction
{
/// <summary>
/// Parses the required user action from a TCP sequence (COLA2 response)
/// Matches: ParseRequiredUserActionData::parseTCPSequence in C++
/// </summary>
public RequiredUserAction ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return ReadRequiredUserAction(span);
}
/// <summary>
/// Matches: ParseRequiredUserActionData::readRequiredUserAction in C++
/// </summary>
private RequiredUserAction ReadRequiredUserAction(ReadOnlySpan<byte> span)
{
ushort word = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
return new RequiredUserAction
{
ConfirmConfiguration = (word & (0x01 << 0)) != 0,
CheckConfiguration = (word & (0x01 << 1)) != 0,
CheckEnvironment = (word & (0x01 << 2)) != 0,
CheckApplicationInterfaces = (word & (0x01 << 3)) != 0,
CheckDevice = (word & (0x01 << 4)) != 0,
RunSetupProcedure = (word & (0x01 << 5)) != 0,
CheckFirmware = (word & (0x01 << 6)) != 0,
Wait = (word & (0x01 << 7)) != 0
};
}
}

View File

@@ -0,0 +1,44 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for SerialNumber response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseSerialNumber
{
/// <summary>
/// Parses the serial number from a TCP sequence (COLA2 response)
/// Matches: ParseSerialNumber::parseTCPSequence in C++
/// </summary>
public static SerialNumber ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new SerialNumber
{
Number = ReadSerialNumber(span)
};
}
/// <summary>
/// Reads serial number from buffer
/// Matches: ParseSerialNumber::readSerialNumber in C++
/// </summary>
private static string ReadSerialNumber(ReadOnlySpan<byte> span)
{
ushort stringLength = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var numberBuilder = new StringBuilder(stringLength);
for (ushort i = 0; i < stringLength; i++)
{
ushort ch = ReadWriteHelper.ReadUint8(span, 2 + i);
numberBuilder.Append((char)ch);
}
return numberBuilder.ToString();
}
}

View File

@@ -0,0 +1,124 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for StatusOverview response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseStatusOverview
{
/// <summary>
/// Parses the status overview from a TCP sequence (COLA2 response)
/// Matches: ParseStatusOverviewData::parseTCPSequence in C++
/// </summary>
public StatusOverview ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new StatusOverview
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
DeviceState = (DeviceState)ReadDeviceState(span),
ConfigState = (ConfigState)ReadConfigState(span),
ApplicationState = (ApplicationState)ReadApplicationState(span),
CurrentTimePowerOnCount = ReadPowerOnCount(span),
CurrentTimeTime = ReadCurrentTime(span),
CurrentTimeDate = ReadCurrentDate(span),
ErrorInfoCode = ReadErrorInfoCode(span),
ErrorInfoTime = ReadErrorInfoTime(span),
ErrorInfoDate = ReadErrorInfoDate(span)
};
}
/// <summary>
/// Matches: ParseStatusOverviewData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseStatusOverviewData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseStatusOverviewData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseStatusOverviewData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
/// <summary>
/// Matches: ParseStatusOverviewData::readDeviceState in C++
/// </summary>
private byte ReadDeviceState(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 4);
}
/// <summary>
/// Matches: ParseStatusOverviewData::readConfigState in C++
/// </summary>
private byte ReadConfigState(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 5);
}
/// <summary>
/// Matches: ParseStatusOverviewData::readApplicationState in C++
/// </summary>
private byte ReadApplicationState(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 6);
}
private uint ReadPowerOnCount(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 12);
}
private uint ReadCurrentTime(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 16);
}
private ushort ReadCurrentDate(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 20);
}
private uint ReadErrorInfoCode(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 24);
}
private uint ReadErrorInfoTime(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 52);
}
private ushort ReadErrorInfoDate(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint16LittleEndian(span, 56);
}
}

View File

@@ -0,0 +1,95 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for incoming TCP packets in COLA2 format
/// Thread-safe implementation
/// </summary>
public sealed class ParseTcpPacket
{
private const int HeaderSize = 18; // COLA2 header size
/// <summary>
/// Gets the expected packet length from the header
/// Matches: ParseTCPPacket::getExpectedPacketLength in C++
/// </summary>
public uint GetExpectedPacketLength(PacketBuffer buffer)
{
var bufferData = buffer.GetBuffer();
var length = ReadWriteHelper.ReadUint32BigEndian(bufferData.Span, 4);
return length + 8; // for STX and Length which is not included in length datafield
}
/// <summary>
/// Gets the request ID from the packet header
/// Matches: ParseTCPPacket::getRequestID in C++
/// </summary>
public ushort GetRequestId(PacketBuffer buffer)
{
var bufferData = buffer.GetBuffer();
return ReadWriteHelper.ReadUint16BigEndian(bufferData.Span, 14);
}
/// <summary>
/// Parses the TCP sequence to extract COLA2 header information
/// Matches: ParseTCPPacket::parseTCPSequence in C++
/// </summary>
public ParseResult ParseTcpSequence(PacketBuffer buffer)
{
var bufferData = buffer.GetBuffer();
var span = bufferData.Span;
// Read header fields
var stx = ReadWriteHelper.ReadUint32BigEndian(span, 0);
var length = ReadWriteHelper.ReadUint32BigEndian(span, 4);
var hubCntr = ReadWriteHelper.ReadUint8BigEndian(span, 8);
var noc = ReadWriteHelper.ReadUint8BigEndian(span, 9);
var sessionId = ReadWriteHelper.ReadUint32BigEndian(span, 10);
var requestId = ReadWriteHelper.ReadUint16BigEndian(span, 14);
var commandType = ReadWriteHelper.ReadUint8BigEndian(span, 16);
var commandMode = ReadWriteHelper.ReadUint8BigEndian(span, 17);
// Read data payload (everything after header, starting at offset 20)
// Matches: ParseTCPPacket::readData in C++ - returns data from offset 20
ReadOnlyMemory<byte> data = ReadOnlyMemory<byte>.Empty;
if (bufferData.Length >= 20)
{
var dataStart = 20;
data = bufferData[dataStart..];
}
return new ParseResult
{
Stx = stx,
Length = length,
HubCntr = hubCntr,
NoC = noc,
SessionId = sessionId,
RequestId = requestId,
CommandType = commandType,
CommandMode = commandMode,
ErrorCode = null,
Data = data
};
}
/// <summary>
/// Result of parsing a TCP packet
/// </summary>
public sealed class ParseResult
{
public uint Stx { get; init; }
public uint Length { get; init; }
public byte HubCntr { get; init; }
public byte NoC { get; init; }
public uint SessionId { get; init; }
public ushort RequestId { get; init; }
public byte CommandType { get; init; }
public byte CommandMode { get; init; }
public ushort? ErrorCode { get; init; }
public ReadOnlyMemory<byte> Data { get; init; }
}
}

View File

@@ -0,0 +1,97 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for TypeCode response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseTypeCode
{
/// <summary>
/// Parses the type code from a TCP sequence (COLA2 response)
/// Matches: ParseTypeCodeData::parseTCPSequence in C++
/// </summary>
public DataStructures.TypeCode ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new DataStructures.TypeCode
{
Code = ReadTypeCode(span),
InterfaceType = ReadInterfaceType(span),
MaxRange = ReadMaxRange(span)
};
}
/// <summary>
/// Matches: ParseTypeCodeData::readTypeCode in C++
/// </summary>
private string ReadTypeCode(ReadOnlySpan<byte> span)
{
ushort codeLength = ReadWriteHelper.ReadUint16LittleEndian(span, 0);
var codeBuilder = new StringBuilder(codeLength);
for (ushort i = 0; i < codeLength; i++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 2 + i);
codeBuilder.Append((char)ch);
}
return codeBuilder.ToString();
}
/// <summary>
/// Matches: ParseTypeCodeData::readInterfaceType in C++
/// </summary>
private InterfaceType ReadInterfaceType(ReadOnlySpan<byte> span)
{
byte typeCodeInterface1 = ReadWriteHelper.ReadUint8(span, 14);
byte typeCodeInterface2 = ReadWriteHelper.ReadUint8(span, 15);
if ((typeCodeInterface1 == 'Z' && typeCodeInterface2 == 'A') ||
(typeCodeInterface1 == 'A' && typeCodeInterface2 == 'A'))
{
return InterfaceType.EfiPro;
}
else if (typeCodeInterface1 == 'I' && typeCodeInterface2 == 'Z')
{
return InterfaceType.EthernetIp;
}
else if ((typeCodeInterface1 == 'P' && typeCodeInterface2 == 'Z') ||
(typeCodeInterface1 == 'L' && typeCodeInterface2 == 'Z'))
{
return InterfaceType.Profinet;
}
else if (typeCodeInterface1 == 'A' && typeCodeInterface2 == 'N')
{
return InterfaceType.NonSafeEthernet;
}
return InterfaceType.EfiPro;
}
/// <summary>
/// Matches: ParseTypeCodeData::readMaxRange in C++
/// </summary>
private double ReadMaxRange(ReadOnlySpan<byte> span)
{
byte typeCodeInterface1 = ReadWriteHelper.ReadUint8(span, 12);
byte typeCodeInterface2 = ReadWriteHelper.ReadUint8(span, 13);
if ((typeCodeInterface1 == '3' && typeCodeInterface2 == '0') ||
(typeCodeInterface1 == '4' && typeCodeInterface2 == '0') ||
(typeCodeInterface1 == '5' && typeCodeInterface2 == '5'))
{
return (double)RangeType.NormalRange;
}
else if (typeCodeInterface1 == '9' && typeCodeInterface2 == '0')
{
return (double)RangeType.LongRange;
}
return (double)RangeType.NormalRange;
}
}

View File

@@ -0,0 +1,89 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Text;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Parser for UserName response from COLA2 variable command
/// Thread-safe implementation
/// </summary>
public sealed class ParseUserName
{
/// <summary>
/// Parses the user name from a TCP sequence (COLA2 response)
/// Matches: ParseUserNameData::parseTCPSequence in C++
/// </summary>
public UserName ParseTcpSequence(PacketBuffer buffer)
{
var span = buffer.GetBufferSpan();
return new UserName
{
VersionCVersion = ReadVersionIndicator(span),
VersionMajorVersionNumber = ReadMajorNumber(span),
VersionMinorVersionNumber = ReadMinorNumber(span),
VersionReleaseNumber = ReadReleaseNumber(span),
NameLength = ReadNameLength(span),
Name = ReadUserName(span)
};
}
/// <summary>
/// Matches: ParseUserNameData::readVersionIndicator in C++
/// </summary>
private string ReadVersionIndicator(ReadOnlySpan<byte> span)
{
byte ch = ReadWriteHelper.ReadUint8(span, 0);
return ((char)ch).ToString();
}
/// <summary>
/// Matches: ParseUserNameData::readMajorNumber in C++
/// </summary>
private byte ReadMajorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 1);
}
/// <summary>
/// Matches: ParseUserNameData::readMinorNumber in C++
/// </summary>
private byte ReadMinorNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 2);
}
/// <summary>
/// Matches: ParseUserNameData::readReleaseNumber in C++
/// </summary>
private byte ReadReleaseNumber(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint8(span, 3);
}
/// <summary>
/// Matches: ParseUserNameData::readNameLength in C++
/// </summary>
private uint ReadNameLength(ReadOnlySpan<byte> span)
{
return ReadWriteHelper.ReadUint32LittleEndian(span, 4);
}
/// <summary>
/// Matches: ParseUserNameData::readUserName in C++
/// </summary>
private string ReadUserName(ReadOnlySpan<byte> span)
{
uint nameLength = ReadWriteHelper.ReadUint32LittleEndian(span, 4);
var nameBuilder = new StringBuilder((int)nameLength);
for (uint i = 0; i < nameLength; i++)
{
byte ch = ReadWriteHelper.ReadUint8(span, 8 + (int)i);
nameBuilder.Append((char)ch);
}
return nameBuilder.ToString();
}
}

View File

@@ -0,0 +1,215 @@
using Sick.SafetyScanners.DataStructures;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Merges TCP packets that may be fragmented across multiple TCP packets
/// Thread-safe implementation - matches C++ logic
/// </summary>
public sealed class TcpPacketMerger : IDisposable
{
private readonly List<PacketBuffer> _packetBuffers;
private uint _targetSize;
private bool _isComplete;
private PacketBuffer? _deployedBuffer;
private bool _disposed;
private readonly object _lock = new();
/// <summary>
/// Creates a new TCP packet merger
/// </summary>
public TcpPacketMerger(uint targetSize = 0)
{
_packetBuffers = new List<PacketBuffer>();
_targetSize = targetSize;
_isComplete = false;
}
/// <summary>
/// Indicates whether the packet is complete
/// </summary>
public bool IsComplete
{
get
{
lock (_lock)
{
return _isComplete;
}
}
}
/// <summary>
/// Indicates whether the buffer is empty
/// </summary>
public bool IsEmpty
{
get
{
lock (_lock)
{
return _packetBuffers.Count == 0;
}
}
}
/// <summary>
/// Gets the target size
/// </summary>
public uint TargetSize
{
get
{
lock (_lock)
{
return _targetSize;
}
}
}
/// <summary>
/// Sets the expected total packet length
/// </summary>
public void SetTargetSize(uint expectedLength)
{
lock (_lock)
{
_targetSize = expectedLength;
}
}
/// <summary>
/// Adds a TCP packet to the merger (stores reference, doesn't copy data yet)
/// Returns true if the packet is complete
/// </summary>
public bool AddTcpPacket(PacketBuffer packet)
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpPacketMerger));
if (packet == null)
throw new ArgumentNullException(nameof(packet));
lock (_lock)
{
// If already complete, reset for new packet
if (_isComplete)
{
_isComplete = false;
_deployedBuffer = null;
}
// Calculate remaining size BEFORE adding packet (matches C++ logic)
var currentSize = GetCurrentSize();
var remainingSize = _targetSize - currentSize;
// Add packet reference (don't copy data yet)
_packetBuffers.Add(packet);
// Check if complete (remaining size should equal packet length)
if (remainingSize == packet.Length)
{
_isComplete = true;
DeployPacketIfComplete();
}
return _isComplete;
}
}
/// <summary>
/// Gets the current total size of all packets
/// </summary>
private uint GetCurrentSize()
{
uint sum = 0;
foreach (var packet in _packetBuffers)
{
sum += (uint)packet.Length;
}
return sum;
}
/// <summary>
/// Deploys packet if complete (merges all packets into one buffer)
/// </summary>
private void DeployPacketIfComplete()
{
if (!_isComplete)
return;
DeployPacket();
}
/// <summary>
/// Merges all packets into a single buffer
/// </summary>
private void DeployPacket()
{
var totalSize = GetCurrentSize();
var mergedBuffer = new List<byte>((int)totalSize);
// Pre-allocate capacity for better performance
foreach (var packet in _packetBuffers)
{
var buffer = packet.GetBuffer();
mergedBuffer.AddRange(buffer.Span);
}
_deployedBuffer = new PacketBuffer(mergedBuffer.ToArray(), mergedBuffer.Count);
_packetBuffers.Clear();
}
/// <summary>
/// Gets the merged packet buffer (only when complete)
/// </summary>
public PacketBuffer GetDeployedBuffer()
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpPacketMerger));
lock (_lock)
{
if (!_isComplete)
throw new InvalidOperationException("Packet is not complete yet");
if (_deployedBuffer == null)
throw new InvalidOperationException("Deployed buffer is null");
// Reset for next packet
_isComplete = false;
var result = _deployedBuffer;
_deployedBuffer = null;
return result;
}
}
/// <summary>
/// Resets the merger for a new packet
/// </summary>
public void Reset()
{
lock (_lock)
{
_packetBuffers.Clear();
_targetSize = 0;
_isComplete = false;
_deployedBuffer = null;
}
}
public void Dispose()
{
if (_disposed)
return;
lock (_lock)
{
_packetBuffers.Clear();
_deployedBuffer = null;
_disposed = true;
}
}
}

View File

@@ -0,0 +1,200 @@
using Sick.SafetyScanners.DataStructures;
namespace Sick.SafetyScanners.DataProcessing;
/// <summary>
/// Merges UDP packets that may be fragmented across multiple UDP packets
/// Thread-safe implementation - matches C++ logic
/// </summary>
public sealed class UdpPacketMerger : IDisposable
{
private readonly Dictionary<uint, List<ParsedPacketBuffer>> _parsedPacketBufferMap = [];
private bool _isComplete = false;
private PacketBuffer? _deployedBuffer;
private bool _disposed;
private readonly Lock _lock = new();
/// <summary>
/// Indicates whether a data packet is complete
/// </summary>
public bool IsComplete
{
get
{
lock (_lock)
{
return _isComplete;
}
}
}
/// <summary>
/// Indicates whether the buffer is empty
/// </summary>
public bool IsEmpty
{
get
{
lock (_lock)
{
return _parsedPacketBufferMap.Count == 0;
}
}
}
/// <summary>
/// Adds a UDP packet to the merger (stores reference, doesn't copy data yet)
/// Returns true if the packet is complete
/// </summary>
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;
}
}
/// <summary>
/// Gets the latest complete data packet
/// </summary>
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;
}
}
/// <summary>
/// Resets the merger (clears all buffers)
/// </summary>
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<ParsedPacketBuffer> list)
{
var totalLength = header.TotalLength;
var currentLength = CalculateCurrentLength(list);
if (currentLength != totalLength)
return false;
return true;
}
private static uint CalculateCurrentLength(List<ParsedPacketBuffer> 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<ParsedPacketBuffer> sortedList)
{
var result = new List<byte>();
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;
}
}