Initial commit
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user