Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
using Sick.SafetyScanners.Cola2.Commands;
using Sick.SafetyScanners.DataProcessing;
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Exceptions;
using Sick.SafetyScanners.Helpers;
using Sick.SafetyScanners.Interfaces;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners.Cola2;
/// <summary>
/// COLA2 session manager for handling send, receive and process telegrams
/// Thread-safe implementation
/// </summary>
public sealed class Cola2Session : ICola2Session
{
private readonly ITcpClient _tcpClient;
private readonly ParseTcpPacket _packetParser;
private uint? _sessionId;
private ushort _requestId;
private readonly object _lock = new();
private bool _disposed;
public uint? SessionId
{
get
{
lock (_lock)
{
return _sessionId;
}
}
private set
{
lock (_lock)
{
_sessionId = value;
}
}
}
public bool IsOpen
{
get
{
// Fast path: read without lock first (volatile-like behavior)
var sessionId = _sessionId;
var isConnected = _tcpClient.IsConnected;
// Double-check with lock if needed
if (!isConnected || !sessionId.HasValue)
return false;
// Verify with lock to ensure consistency
lock (_lock)
{
return _tcpClient.IsConnected && _sessionId.HasValue;
}
}
}
/// <summary>
/// Creates a new COLA2 session
/// </summary>
public Cola2Session(ITcpClient tcpClient)
{
_tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient));
_packetParser = new ParseTcpPacket();
_requestId = 0;
}
public ushort GetNextRequestId()
{
lock (_lock)
{
return ++_requestId;
}
}
public void Open()
{
if (_disposed)
ObjectDisposedException.ThrowIf(_disposed, nameof(Cola2Session));
// Close existing session first if open (avoid deadlock by not locking)
if (IsOpen)
{
Close();
}
// Connect TCP if not connected
if (!_tcpClient.IsConnected)
{
_tcpClient.Connect(TimeDuration.FromSeconds(5));
}
// Create session command
var createSessionCmd = new CreateSessionCommand();
// Send command without session ID check
if (!createSessionCmd.CanBeExecutedWithoutSessionId)
{
throw new InvalidOperationException("CreateSession command must be executable without session ID");
}
// Manually set request ID
createSessionCmd.RequestId = GetNextRequestId();
createSessionCmd.SessionId = 0; // No session ID for CreateSession
// Assemble and send telegram (GỬI CreateSession command)
AssembleAndSendTelegram(createSessionCmd);
// Receive and process response (NHẬN response từ scanner)
var response = ReceiveAndProcessResponse(createSessionCmd,
TimeDuration.FromSeconds(5));
// Parse response
var parseResult = _packetParser.ParseTcpSequence(response);
// Process reply
createSessionCmd.ProcessReply(parseResult.Data, parseResult.CommandType, parseResult.CommandMode);
// Check for error code
if (parseResult.ErrorCode.HasValue && parseResult.ErrorCode.Value != 0)
{
throw new CommandException(
0,
parseResult.RequestId,
parseResult.CommandType,
parseResult.CommandMode,
parseResult.ErrorCode.Value,
$"CreateSession failed with error code: 0x{parseResult.ErrorCode.Value:X4}"
);
}
if (!createSessionCmd.WasSuccessful)
{
throw new SessionException("Failed to create COLA2 session");
}
// Extract session ID from packet header (not from reply data)
// In C++ reference, session ID is read from packet header (offset 10) by ParseTCPPacket
// and set to Command via setCommandValuesFromPacket(), then retrieved via getSessionID()
// In C# implementation, we get it directly from parseResult.SessionId
if (parseResult.SessionId == 0)
{
throw new SessionException("Failed to extract session ID from CreateSession reply: Session ID is 0");
}
SessionId = parseResult.SessionId;
}
public void Close()
{
ObjectDisposedException.ThrowIf(_disposed, nameof(Cola2Session));
lock (_lock)
{
if (!IsOpen)
{
// Already closed
return;
}
}
try
{
var closeSessionCmd = new CloseSessionCommand
{
SessionId = SessionId ?? 0
};
SendCommand(closeSessionCmd, TimeDuration.FromSeconds(5));
}
catch (Exception)
{
// Ignore errors during close
}
finally
{
SessionId = null;
_tcpClient.Disconnect();
}
}
public void SendCommand(ICola2Command command, TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(Cola2Session));
ArgumentNullException.ThrowIfNull(command);
// Check if session is required
lock (_lock)
{
if (!command.CanBeExecutedWithoutSessionId && !IsOpen)
{
throw new SessionException("Attempt to send command in closed Cola2 session state");
}
// Set session ID and request ID
// In C++ reference: cmd.setSessionID(getSessionID().get_value_or(0));
// We must ensure session ID is set correctly before sending
if (!_sessionId.HasValue && !command.CanBeExecutedWithoutSessionId)
{
throw new SessionException($"Cannot send command: Session ID is not set. Session is open: {IsOpen}");
}
command.SessionId = _sessionId ?? 0;
command.RequestId = GetNextRequestId();
// Debug: Verify session ID is set (only for commands that require session)
if (!command.CanBeExecutedWithoutSessionId && command.SessionId == 0)
{
throw new SessionException($"Session ID is 0 for command that requires session. Current session state: IsOpen={IsOpen}, _sessionId={_sessionId}");
}
}
// Assemble and send telegram
AssembleAndSendTelegram(command);
// Receive and process response
var response = ReceiveAndProcessResponse(command, timeout);
// Parse response
var parseResult = _packetParser.ParseTcpSequence(response);
// Check error code FIRST - if error code is present and non-zero, it indicates an error
// even if command type/mode is correct (R/A)
// Error code format: 0x[VARIABLE_INDEX]00 typically means "variable index not found"
if (parseResult.ErrorCode.HasValue && parseResult.ErrorCode.Value != 0)
{
var errorCode = parseResult.ErrorCode.Value;
var errorMessage = GetErrorMessage(errorCode, command);
throw new CommandException(
SessionId ?? 0,
parseResult.RequestId,
parseResult.CommandType,
parseResult.CommandMode,
errorCode,
errorMessage
);
}
// Process reply (this checks command type/mode and sets WasSuccessful)
command.ProcessReply(parseResult.Data, parseResult.CommandType, parseResult.CommandMode);
// Check if processReply failed (command type/mode mismatch)
if (!command.WasSuccessful)
{
// When command type/mode is wrong, check for error code in data
ushort? errorCode = null;
if (parseResult.Data.Length >= 2)
{
// Error code might be at the start of data payload (offset 18 of packet)
errorCode = ReadWriteHelper.ReadUint16BigEndian(parseResult.Data.Span, 0);
}
throw new CommandException(SessionId ?? 0,
parseResult.RequestId,
parseResult.CommandType,
parseResult.CommandMode,
parseResult.ErrorCode ?? 0,
$"Command failed: processReply returned false. Expected 'R'/'A' (0x52/0x41), got 0x{parseResult.CommandType:X2}/0x{parseResult.CommandMode:X2}"
);
}
// Command was successful (command type/mode is 'R'/'A')
// The 2 bytes at offset 18-19 are part of the data payload (variable index for VariableCommand)
// NOT an error code - error code only exists when command fails
// No need to check error code when command is acknowledged
// Command was successful (processReply returned true and no error code or error code matches variable index)
}
private void AssembleAndSendTelegram(ICola2Command command)
{
if (command is not CommandBase cmdBase)
throw new ArgumentException("Command must be derived from CommandBase", nameof(command));
var telegram = cmdBase.ConstructTelegram();
_tcpClient.Send(telegram);
}
private PacketBuffer ReceiveAndProcessResponse(ICola2Command command,
TimeDuration? timeout)
{
var packetMerger = new TcpPacketMerger();
try
{
while (!packetMerger.IsComplete)
{
var packet = _tcpClient.Receive(timeout);
if (packetMerger.IsEmpty)
{
var expectedLength = _packetParser.GetExpectedPacketLength(packet);
packetMerger.SetTargetSize(expectedLength);
}
// AddTcpPacket returns true if complete
var isComplete = packetMerger.AddTcpPacket(packet);
if (isComplete)
{
break; // Exit loop early when complete
}
}
return packetMerger.GetDeployedBuffer();
}
finally
{
packetMerger.Dispose();
}
}
private string GetErrorMessage(ushort errorCode, ICola2Command command)
{
// Common COLA2 error codes
// 0x0D00 = Variable index not found or not supported
// 0x0001 = General error
// 0x0002 = Invalid parameter
// etc.
var baseMessage = $"Command failed with error code: 0x{errorCode:X4}";
if (command is VariableCommand varCmd)
{
baseMessage += $" (Variable Index: 0x{varCmd.VariableIndex:X4})";
// Common error codes for variable commands
if (errorCode == 0x0D00)
{
baseMessage += " - Variable index not found or not supported by this scanner model";
}
else if (errorCode == 0x0001)
{
baseMessage += " - General error";
}
else if (errorCode == 0x0002)
{
baseMessage += " - Invalid parameter";
}
}
return baseMessage;
}
public void Dispose()
{
if (_disposed)
return;
Close();
_tcpClient.Dispose();
_disposed = true;
}
}

View File

@@ -0,0 +1,122 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
using System.Net;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Command to change communication settings on the sensor
/// </summary>
public sealed class ChangeCommSettingsCommand : MethodCommand
{
private readonly CommSettings _settings;
public ChangeCommSettingsCommand(CommSettings settings)
: base(0x00b0) // Method index for ChangeCommSettings
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
public CommSettings Settings => _settings;
public override bool CanBeExecutedWithoutSessionId => true;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
// Base method index (2 bytes) + 28 bytes for settings data
var data = new byte[2 + 28];
var span = data.AsSpan();
// Write base method index (from MethodCommand.AddTelegramData)
ReadWriteHelper.WriteUint16LittleEndian(span, 0, MethodIndex);
// Write settings data starting at offset 2 (after method index)
WriteDataToSpan(span.Slice(2));
return data;
}
private void WriteDataToSpan(Span<byte> span)
{
// Channel (offset 0)
ReadWriteHelper.WriteUint8LittleEndian(span, 0, _settings.Channel);
// Skip 3 bytes (offsets 1, 2, 3)
// Enabled (offset 4)
ReadWriteHelper.WriteUint8LittleEndian(span, 4, (byte)(_settings.Enabled ? 1 : 0));
// Interface type (offset 5)
ReadWriteHelper.WriteUint8LittleEndian(span, 5, (byte)_settings.EInterfaceType);
// Skip 2 bytes (offsets 6, 7)
// Host IP (offset 8, 4 bytes, little endian)
if (!IPAddress.TryParse(_settings.HostIp, out var ipAddress) ||
ipAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
{
throw new ArgumentException($"Invalid IPv4 address: {_settings.HostIp}", nameof(_settings));
}
var bytes = ipAddress.GetAddressBytes();
if (bytes.Length != 4)
{
throw new InvalidOperationException($"IPAddress.GetAddressBytes() returned {bytes.Length} bytes, expected 4");
}
// Convert to uint32 (little endian)
// IPAddress.GetAddressBytes() returns bytes in network byte order (big endian): [b0, b1, b2, b3]
// For little endian uint32, we need: bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)
// uint ipUint = (uint)(bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24));
uint ipUint = (uint)(bytes[3] | (bytes[2] << 8) | (bytes[1] << 16) | (bytes[0] << 24));
ReadWriteHelper.WriteUint32LittleEndian(span, 8, ipUint);
// Host UDP port (offset 12, 2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 12, _settings.HostUdpPort);
// Publishing frequency (offset 14, 2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 14, _settings.PublishingFrequency);
// Start angle (offset 16, 4 bytes, little endian, multiplied by 4194304.0)
int startAngleInt = (int)(_settings.StartAngle * 4194304.0);
ReadWriteHelper.WriteInt32LittleEndian(span, 16, startAngleInt);
// End angle (offset 20, 4 bytes, little endian, multiplied by 4194304.0)
int endAngleInt = (int)(_settings.EndAngle * 4194304.0);
ReadWriteHelper.WriteInt32LittleEndian(span, 20, endAngleInt);
// Features (offset 24, 2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 24, _settings.Features);
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// According to C++ reference, this inverts the result from base class
// Base class returns true for 'A' + 'I' (Acknowledge)
// But ChangeCommSettingsCommand expects different reply format
// Let's check for error response: if we get 'E' + 'I' (Error), return false
// Otherwise, check base class logic
// Error response: 'E' (0x45) and 'I' (0x49)
if ((replyCommandType == 0x45 && replyCommandMode == 0x49) ||
(replyCommandType == 'E' && replyCommandMode == 'I'))
{
return false;
}
// Success: 'A' (0x41) and 'I' (0x49)
if ((replyCommandType == 0x41 && replyCommandMode == 0x49) ||
(replyCommandType == 'A' && replyCommandMode == 'I'))
{
return true;
}
// For ChangeCommSettings, the C++ code inverts the base result
// which suggests it might have different error handling
// Let's use standard acknowledge check
return (replyCommandType == 0x41 && replyCommandMode == 0x49) ||
(replyCommandType == 'A' && replyCommandMode == 'I');
}
}

View File

@@ -0,0 +1,34 @@
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Command to close a COLA2 session
/// </summary>
public sealed class CloseSessionCommand : CommandBase
{
public CloseSessionCommand()
: base(0x43, 0x58) // 'C' and 'X' in ASCII
{
}
public override bool CanBeExecutedWithoutSessionId => false;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
// Close session command has no additional data
return ReadOnlyMemory<byte>.Empty;
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// Reply should be 'C' (0x43) and 'A' (0x41) for Acknowledge
if ((replyCommandType == 0x43 && replyCommandMode == 0x41) ||
(replyCommandType == 'C' && replyCommandMode == 'A'))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,201 @@
using Sick.SafetyScanners.Cola2.Commands;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Base class for all COLA2 commands
/// Thread-safe implementation
/// </summary>
public abstract class CommandBase : ICola2Command
{
private readonly byte _commandType;
private readonly byte _commandMode;
private uint _sessionId;
private ushort _requestId;
private bool _wasSuccessful;
private readonly List<byte> _dataVector;
private readonly object _lock = new();
protected CommandBase(byte commandType, byte commandMode)
{
_commandType = commandType;
_commandMode = commandMode;
_dataVector = new List<byte>();
}
public byte CommandType => _commandType;
public byte CommandMode => _commandMode;
public uint SessionId
{
get
{
lock (_lock)
{
return _sessionId;
}
}
set
{
lock (_lock)
{
_sessionId = value;
}
}
}
public ushort RequestId
{
get
{
lock (_lock)
{
return _requestId;
}
}
set
{
lock (_lock)
{
_requestId = value;
}
}
}
public bool WasSuccessful
{
get
{
lock (_lock)
{
return _wasSuccessful;
}
}
protected set
{
lock (_lock)
{
_wasSuccessful = value;
}
}
}
public abstract bool CanBeExecutedWithoutSessionId { get; }
/// <summary>
/// Gets the data vector for the command payload
/// </summary>
public ReadOnlyMemory<byte> GetDataVector()
{
lock (_lock)
{
return _dataVector.ToArray();
}
}
/// <summary>
/// Sets the data vector
/// </summary>
protected void SetDataVector(ReadOnlyMemory<byte> data)
{
lock (_lock)
{
_dataVector.Clear();
_dataVector.AddRange(data.ToArray());
}
}
/// <summary>
/// Adds data to the data vector
/// </summary>
protected void AddData(ReadOnlyMemory<byte> data)
{
lock (_lock)
{
_dataVector.AddRange(data.ToArray());
}
}
/// <summary>
/// Constructs the complete telegram including header
/// </summary>
public byte[] ConstructTelegram()
{
lock (_lock)
{
var data = AddTelegramData();
return AddTelegramHeader(data);
}
}
/// <summary>
/// Adds command-specific data to the telegram
/// </summary>
protected abstract ReadOnlyMemory<byte> AddTelegramData();
/// <summary>
/// Processes the reply from the sensor
/// In C++ reference, ParseTCPPacket::parseTCPSequence calls command.setDataVector(byte_vector)
/// to store the reply data in the command. We need to do the same here.
/// </summary>
public bool ProcessReply(ReadOnlyMemory<byte> replyData, byte replyCommandType, byte replyCommandMode)
{
lock (_lock)
{
// Store reply data in _dataVector (matching C++ reference behavior)
// In C++: command.setDataVector(byte_vector) is called by ParseTCPPacket
SetDataVector(replyData);
_wasSuccessful = ProcessReplyInternal(replyData, replyCommandType, replyCommandMode);
return _wasSuccessful;
}
}
/// <summary>
/// Internal method to process reply (implemented by derived classes)
/// </summary>
protected abstract bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode);
/// <summary>
/// Adds the COLA2 header to the telegram
/// </summary>
private byte[] AddTelegramHeader(ReadOnlyMemory<byte> data)
{
const int headerSize = 18;
var totalLength = headerSize + data.Length;
var telegram = new byte[totalLength];
var span = telegram.AsSpan();
// STX (4 bytes): 0x02020202
ReadWriteHelper.WriteUint32BigEndian(span, 0, 0x02020202);
// Length (4 bytes): 10 + data.Length
ReadWriteHelper.WriteUint32BigEndian(span, 4, (uint)(10 + data.Length));
// HubCntr (1 byte): 0x00
ReadWriteHelper.WriteUint8BigEndian(span, 8, 0x00);
// NoC (1 byte): 0x00
ReadWriteHelper.WriteUint8BigEndian(span, 9, 0x00);
// Session ID (4 bytes)
ReadWriteHelper.WriteUint32BigEndian(span, 10, _sessionId);
// Request ID (2 bytes)
ReadWriteHelper.WriteUint16BigEndian(span, 14, _requestId);
// Command Type (1 byte)
ReadWriteHelper.WriteUint8BigEndian(span, 16, _commandType);
// Command Mode (1 byte)
ReadWriteHelper.WriteUint8BigEndian(span, 17, _commandMode);
// Copy data
data.CopyTo(telegram.AsMemory(headerSize));
return telegram;
}
}

View File

@@ -0,0 +1,50 @@
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Command to create a new COLA2 session
/// </summary>
public sealed class CreateSessionCommand : CommandBase
{
private const byte HeartbeatTimeoutSeconds = 60;
private const uint ClientId = 1;
public CreateSessionCommand()
: base(0x4F, 0x58) // 'O' and 'X' in ASCII
{
}
public override bool CanBeExecutedWithoutSessionId => true;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
var data = new byte[5];
var span = data.AsSpan();
// Heartbeat timeout (1 byte)
ReadWriteHelper.WriteUint8BigEndian(span, 0, HeartbeatTimeoutSeconds);
// Client ID (4 bytes)
ReadWriteHelper.WriteUint32BigEndian(span, 1, ClientId);
return data;
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// Reply should be 'O' (0x4F) and 'A' (0x41) for Acknowledge
// Note: Session ID is NOT in reply data, it's in the packet header (offset 10)
// In C++ reference, ParseTCPPacket reads session ID from header and sets it to Command
// In C# implementation, Cola2Session extracts it from parseResult.SessionId
if ((replyCommandType == 0x4F && replyCommandMode == 0x41) ||
(replyCommandType == 'O' && replyCommandMode == 'A'))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,53 @@
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Command to make the scanner flash/blink to help locate it
/// </summary>
public sealed class FindMeCommand : MethodCommand
{
private readonly ushort _blinkTime;
public FindMeCommand(ushort blinkTime)
: base(14) // Method index for FindMe
{
_blinkTime = blinkTime;
}
public ushort BlinkTime => _blinkTime;
public override bool CanBeExecutedWithoutSessionId => true;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
// Base method index (2 bytes) + blink time (2 bytes)
var data = new byte[2 + 2];
var span = data.AsSpan();
// Write base method index (from MethodCommand.AddTelegramData)
ReadWriteHelper.WriteUint16LittleEndian(span, 0, MethodIndex);
// Write blink time (offset 2, 2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 2, _blinkTime);
return data;
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// According to C++ reference, this inverts the result from base class
// Error response: 'E' (0x45) and 'I' (0x49)
if ((replyCommandType == 0x45 && replyCommandMode == 0x49) ||
(replyCommandType == 'E' && replyCommandMode == 'I'))
{
return false;
}
// Success: 'A' (0x41) and 'I' (0x49)
return (replyCommandType == 0x41 && replyCommandMode == 0x49) ||
(replyCommandType == 'A' && replyCommandMode == 'I');
}
}

View File

@@ -0,0 +1,80 @@
using Sick.SafetyScanners.DataProcessing;
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Command to request the latest telegram (measurement data) from the sensor via TCP
/// Variable index: 179 + channel_index (179 for channel 0, 180 for channel 1, etc.)
/// </summary>
public sealed class LatestTelegramVariableCommand : CommandBase
{
private readonly ushort _variableIndex;
private readonly ParseData _dataParser;
private UdpScanData? _scanData;
/// <summary>
/// Creates a new LatestTelegramVariableCommand
/// </summary>
/// <param name="channelIndex">Channel index (0-3), defaults to 0</param>
public LatestTelegramVariableCommand(sbyte channelIndex = 0)
: base(0x52, 0x49) // 'R' and 'I' in ASCII (Read by Index)
{
if (channelIndex < 0 || channelIndex > 3)
{
throw new ArgumentOutOfRangeException(nameof(channelIndex),
"Channel index must be between 0 and 3");
}
// Variable index: 179 + channel_index
_variableIndex = (ushort)(179 + channelIndex);
_dataParser = new ParseData();
}
public ushort VariableIndex => _variableIndex;
public override bool CanBeExecutedWithoutSessionId => true;
/// <summary>
/// Gets the parsed scan data after the command has been executed successfully
/// </summary>
public UdpScanData? ScanData => _scanData;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
var data = new byte[2];
var span = data.AsSpan();
// Variable index (2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 0, _variableIndex);
return data;
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// Reply should be 'R' (0x52) and 'A' (0x41) for Acknowledge
if ((replyCommandType == 0x52 && replyCommandMode == 0x41) ||
(replyCommandType == 'R' && replyCommandMode == 'A'))
{
try
{
// Parse the TCP sequence data
// The replyData contains the measurement data payload
var packetBuffer = new PacketBuffer(replyData.ToArray(), replyData.Length);
_scanData = _dataParser.ParseTcpSequence(packetBuffer);
return true;
}
catch (Exception)
{
// Parsing failed
return false;
}
}
return false;
}
}

View File

@@ -0,0 +1,47 @@
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Base command for method calls to the sensor
/// Thread-safe implementation
/// </summary>
public abstract class MethodCommand : CommandBase
{
private readonly ushort _methodIndex;
protected MethodCommand(ushort methodIndex)
: base(0x4D, 0x49) // 'M' and 'I' in ASCII (Method by Index)
{
_methodIndex = methodIndex;
}
public ushort MethodIndex => _methodIndex;
public override bool CanBeExecutedWithoutSessionId => false;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
var data = new byte[2];
var span = data.AsSpan();
// Method index (2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 0, _methodIndex);
return data;
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// Reply should be 'A' (0x41) and 'I' (0x49) for Acknowledge
if ((replyCommandType == 0x41 && replyCommandMode == 0x49) ||
(replyCommandType == 'A' && replyCommandMode == 'I'))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,46 @@
using Sick.SafetyScanners.Helpers;
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Command to read a variable from the sensor by index
/// </summary>
public sealed class VariableCommand : CommandBase
{
private readonly ushort _variableIndex;
public VariableCommand(ushort variableIndex)
: base(0x52, 0x49) // 'R' and 'I' in ASCII (Read by Index)
{
_variableIndex = variableIndex;
}
public ushort VariableIndex => _variableIndex;
public override bool CanBeExecutedWithoutSessionId => false;
protected override ReadOnlyMemory<byte> AddTelegramData()
{
var data = new byte[2];
var span = data.AsSpan();
// Variable index (2 bytes, little endian)
ReadWriteHelper.WriteUint16LittleEndian(span, 0, _variableIndex);
return data;
}
protected override bool ProcessReplyInternal(ReadOnlyMemory<byte> replyData,
byte replyCommandType, byte replyCommandMode)
{
// Reply should be 'R' (0x52) and 'A' (0x41) for Acknowledge
if ((replyCommandType == 0x52 && replyCommandMode == 0x41) ||
(replyCommandType == 'R' && replyCommandMode == 'A'))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,207 @@
using System.Net;
using System.Net.Sockets;
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Exceptions;
using Sick.SafetyScanners.Interfaces;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners.Communication;
/// <summary>
/// Synchronous TCP client for COLA2 communication
/// Thread-safe implementation
/// </summary>
public sealed class TcpClient : ITcpClient
{
private readonly IpAddress _serverIp;
private readonly Port _serverPort;
private System.Net.Sockets.TcpClient? _tcpClient;
private NetworkStream? _stream;
private readonly object _lock = new();
private bool _disposed;
public IpAddress ServerIp => _serverIp;
public Port ServerPort => _serverPort;
public bool IsConnected
{
get
{
lock (_lock)
{
return _tcpClient?.Connected == true && _stream != null;
}
}
}
/// <summary>
/// Creates a new TCP client
/// </summary>
public TcpClient(IpAddress serverIp, Port serverPort)
{
_serverIp = serverIp;
_serverPort = serverPort;
}
/// <summary>
/// Creates a new TCP client from string IP and port
/// </summary>
public TcpClient(string serverIp, ushort serverPort)
: this(new IpAddress(serverIp), new Port(serverPort))
{
}
public void Connect(TimeDuration? timeout = null)
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpClient));
var timeoutDuration = timeout?.ToTimeSpan() ?? TimeSpan.FromSeconds(5);
lock (_lock)
{
if (IsConnected)
{
// Already connected
return;
}
_tcpClient?.Dispose();
_tcpClient = new System.Net.Sockets.TcpClient();
}
try
{
var connectResult = _tcpClient!.BeginConnect(_serverIp.ToIPAddress(), _serverPort.Value, null, null);
var success = connectResult.AsyncWaitHandle.WaitOne(timeoutDuration);
if (!success)
{
_tcpClient.Dispose();
_tcpClient = null;
throw new Cola2TimeoutException("Connect", timeoutDuration,
$"Timeout exceeded while connecting to {_serverIp}:{_serverPort}");
}
_tcpClient.EndConnect(connectResult);
lock (_lock)
{
_stream = _tcpClient.GetStream();
}
}
catch (Exception ex) when (!(ex is Cola2TimeoutException || ex is TcpCommunicationException))
{
lock (_lock)
{
_tcpClient?.Dispose();
_tcpClient = null;
}
throw new TcpCommunicationException(_serverIp.ToString(), _serverPort.Value,
"Connection failed", ex);
}
}
public void Disconnect()
{
lock (_lock)
{
if (!IsConnected)
return;
_stream?.Dispose();
_stream = null;
_tcpClient?.Close();
_tcpClient?.Dispose();
_tcpClient = null;
}
}
public void Send(ReadOnlyMemory<byte> data)
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpClient));
NetworkStream? stream;
lock (_lock)
{
if (!IsConnected)
throw new TcpCommunicationException(_serverIp.ToString(), _serverPort.Value,
"Cannot send data: not connected");
stream = _stream;
}
if (stream == null)
throw new InvalidOperationException("Stream is null");
try
{
stream.Write(data.Span);
stream.Flush();
}
catch (Exception ex)
{
throw new TcpCommunicationException(_serverIp.ToString(), _serverPort.Value,
"Failed to send data", ex);
}
}
public PacketBuffer Receive(TimeDuration? timeout = null)
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpClient));
NetworkStream? stream;
lock (_lock)
{
if (!IsConnected)
throw new TcpCommunicationException(_serverIp.ToString(), _serverPort.Value,
"Cannot receive data: not connected");
stream = _stream;
}
if (stream == null)
throw new InvalidOperationException("Stream is null");
var timeoutDuration = timeout?.ToTimeSpan() ?? TimeSpan.FromSeconds(5);
// Use MaxSize to match C++ implementation (MAXSIZE = 10000)
var buffer = new byte[PacketBuffer.MaxSize];
try
{
// Set read timeout
stream.ReadTimeout = (int)timeoutDuration.TotalMilliseconds;
var bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
throw new TcpCommunicationException(_serverIp.ToString(), _serverPort.Value,
"Connection closed by remote host");
}
return new PacketBuffer(buffer, bytesRead);
}
catch (Exception ex) when (!(ex is Cola2TimeoutException || ex is TcpCommunicationException))
{
throw new TcpCommunicationException(_serverIp.ToString(), _serverPort.Value,
"Failed to receive data", ex);
}
}
public void Dispose()
{
if (_disposed)
return;
lock (_lock)
{
_stream?.Dispose();
_tcpClient?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,313 @@
using System;
using System.Net;
using System.Net.Sockets;
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Interfaces;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners.Communication;
/// <summary>
/// UDP receiver for receiving scan data packets from SICK Safety Scanner
/// Note: UDP is connectionless - this is NOT a connection, just a UDP socket receiver.
/// The scanner will send UDP packets to this local port (push mode).
/// Thread-safe implementation
/// </summary>
public sealed class UdpClient : IUdpClient
{
private readonly ushort _localPort;
private readonly IPAddress? _localIp;
private System.Net.Sockets.UdpClient? _socket;
private bool _isReceiving;
private Thread? _receiveThread;
private readonly object _lock = new();
private bool _disposed;
/// <summary>
/// Creates a new UDP client
/// </summary>
/// <param name="localPort">Local port number to bind to (0 = auto-assign)</param>
public UdpClient(ushort localPort = 0)
: this(localPort, null)
{
}
/// <summary>
/// Creates a new UDP client with specific local IP address
/// </summary>
/// <param name="localPort">Local port number to bind to (0 = auto-assign)</param>
/// <param name="localIp">Local IP address to bind to (null = bind to 0.0.0.0, all interfaces)</param>
public UdpClient(ushort localPort, IPAddress? localIp)
{
_localPort = localPort;
_localIp = localIp;
}
/// <summary>
/// Indicates whether the UDP client is connected (socket is open)
/// </summary>
public bool IsConnected
{
get
{
lock (_lock)
{
return _socket != null && !_disposed;
}
}
}
/// <summary>
/// Gets the local port number assigned to this client
/// Returns null if socket is not bound yet
/// </summary>
public Port? LocalPort
{
get
{
lock (_lock)
{
if (_socket == null)
return null;
try
{
var localEndPoint = (IPEndPoint?)_socket.Client.LocalEndPoint;
var port = localEndPoint?.Port ?? 0;
if (port == 0)
return null;
return new Port((ushort)port);
}
catch
{
return null;
}
}
}
}
/// <summary>
/// Indicates whether data is available in the receiving buffer
/// </summary>
public bool IsDataAvailable
{
get
{
lock (_lock)
{
if (_socket == null || _disposed)
return false;
try
{
return _socket.Available > 0;
}
catch
{
return false;
}
}
}
}
/// <summary>
/// Starts receiving UDP packets on a dedicated high-priority thread
/// </summary>
public void StartReceiving(Action<PacketBuffer> packetHandler)
{
if (_disposed)
throw new ObjectDisposedException(nameof(UdpClient));
if (packetHandler == null)
throw new ArgumentNullException(nameof(packetHandler));
lock (_lock)
{
if (_isReceiving)
throw new InvalidOperationException("UDP client is already receiving");
// Create UDP client if not exists
// This will bind the socket immediately
if (_socket == null)
{
if (_localIp != null)
{
// Bind to specific IP address
var localEndPoint = new IPEndPoint(_localIp, _localPort);
_socket = new System.Net.Sockets.UdpClient(localEndPoint);
}
else
{
// Bind to 0.0.0.0 (all interfaces) - default behavior
_socket = new System.Net.Sockets.UdpClient(_localPort);
}
}
_isReceiving = true;
}
// Wait a bit to ensure socket is bound (LocalEndPoint is set)
// This is needed because socket binding might not be immediate
Thread.Sleep(10);
// Start receiving loop on dedicated high-priority thread
_receiveThread = new Thread(() =>
{
ReceiveLoop(packetHandler);
})
{
Priority = ThreadPriority.Highest,
IsBackground = false,
Name = $"UDPReceive-{_localPort}"
};
_receiveThread.Start();
}
/// <summary>
/// Stops the receiving thread
/// </summary>
public void Stop()
{
Thread? receiveThread;
lock (_lock)
{
if (!_isReceiving)
return;
_isReceiving = false;
receiveThread = _receiveThread;
}
// Wait for thread to finish (with timeout)
if (receiveThread != null)
{
if (!receiveThread.Join(TimeSpan.FromSeconds(2)))
{
// Thread didn't finish in time, but continue cleanup
}
}
lock (_lock)
{
_receiveThread = null;
}
}
private void ReceiveLoop(Action<PacketBuffer> packetHandler)
{
Thread.BeginThreadAffinity();
try
{
System.Net.Sockets.UdpClient? socket;
lock (_lock)
{
socket = _socket;
}
if (socket == null)
return;
while (true)
{
lock (_lock)
{
if (!_isReceiving)
break;
}
try
{
var endPoint = new IPEndPoint(IPAddress.Any, 0);
var result = socket.Receive(ref endPoint);
var packetBuffer = new PacketBuffer(result, result.Length);
packetHandler(packetBuffer);
}
catch (SocketException ex)
{
lock (_lock)
{
if (!_isReceiving)
break;
}
// Log error but continue receiving
// In production, you might want to fire an error event
System.Diagnostics.Debug.WriteLine($"UDP receive error: {ex.Message}");
// Small delay before retrying to avoid tight loop
Thread.Sleep(100);
}
catch (ObjectDisposedException)
{
// Socket was disposed, exit loop
break;
}
}
}
finally
{
Thread.EndThreadAffinity();
}
}
public void Dispose()
{
if (_disposed)
return;
Thread? receiveThread;
// Step 1: Set _isReceiving = false to allow loop to exit immediately
lock (_lock)
{
if (!_isReceiving)
{
// Already stopped, just cleanup
receiveThread = null;
}
else
{
_isReceiving = false;
receiveThread = _receiveThread;
}
}
// Step 2: Dispose socket to unblock Receive() call
// This allows the receive thread to exit quickly from blocking Receive()
lock (_lock)
{
_socket?.Close();
_socket?.Dispose();
_socket = null;
}
// Step 3: Wait for receive thread to finish (with timeout to avoid hanging)
if (receiveThread != null)
{
try
{
if (!receiveThread.Join(TimeSpan.FromSeconds(2)))
{
// Thread didn't finish in time, but continue cleanup
}
}
catch (Exception)
{
// Ignore errors waiting for thread
}
}
// Step 4: Cleanup
lock (_lock)
{
_receiveThread = null;
}
_disposed = true;
GC.SuppressFinalize(this);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Bundles application input and output data
/// </summary>
public sealed class ApplicationData
{
/// <summary>
/// Gets or sets the application inputs
/// </summary>
public ApplicationInputs Inputs { get; init; } = new ApplicationInputs();
/// <summary>
/// Gets or sets the application outputs
/// </summary>
public ApplicationOutputs Outputs { get; init; } = new ApplicationOutputs();
/// <summary>
/// Gets whether application data is empty (not enabled)
/// </summary>
public bool IsEmpty { get; init; }
}

View File

@@ -0,0 +1,62 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains application inputs from a UDP data packet
/// </summary>
public sealed class ApplicationInputs
{
/// <summary>
/// Gets or sets the unsafe input sources (bits represent current state of static input sources)
/// </summary>
public IReadOnlyList<bool> UnsafeInputsInputSources { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets the flags for unsafe input sources (one flag per static input source)
/// </summary>
public IReadOnlyList<bool> UnsafeInputsFlags { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets the monitoring case numbers
/// </summary>
public IReadOnlyList<ushort> MonitoringCases { get; init; } = Array.Empty<ushort>();
/// <summary>
/// Gets or sets the monitoring case flags
/// </summary>
public IReadOnlyList<bool> MonitoringCaseFlags { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets the first linear velocity input
/// </summary>
public short Velocity0 { get; init; }
/// <summary>
/// Gets or sets the second linear velocity input
/// </summary>
public short Velocity1 { get; init; }
/// <summary>
/// Gets or sets whether first linear velocity input is valid
/// </summary>
public bool IsVelocity0Valid { get; init; }
/// <summary>
/// Gets or sets whether second linear velocity input is valid
/// </summary>
public bool IsVelocity1Valid { get; init; }
/// <summary>
/// Gets or sets whether first linear velocity input is transmitted safely
/// </summary>
public bool IsVelocity0TransmittedSafely { get; init; }
/// <summary>
/// Gets or sets whether second linear velocity input is transmitted safely
/// </summary>
public bool IsVelocity1TransmittedSafely { get; init; }
/// <summary>
/// Gets or sets the state of the sleep mode input
/// </summary>
public sbyte SleepModeInput { get; init; }
}

View File

@@ -0,0 +1,38 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the application name from a COLA2 variable command response
/// </summary>
public sealed class ApplicationName
{
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets the length of the application name
/// </summary>
public uint NameLength { get; init; }
/// <summary>
/// Gets or sets the application name string
/// </summary>
public string Name { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,117 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains application outputs from a UDP data packet
/// </summary>
public sealed class ApplicationOutputs
{
/// <summary>
/// Gets or sets the state of the non-safe cut-off paths (eval out)
/// </summary>
public IReadOnlyList<bool> EvalOut { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets whether a cut-off path from the output paths is safe
/// </summary>
public IReadOnlyList<bool> EvalOutIsSafe { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets whether the output path is valid
/// </summary>
public IReadOnlyList<bool> EvalOutIsValid { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets the currently active monitoring case numbers
/// </summary>
public IReadOnlyList<ushort> MonitoringCases { get; init; } = Array.Empty<ushort>();
/// <summary>
/// Gets or sets whether the corresponding monitoring case number is valid
/// </summary>
public IReadOnlyList<bool> MonitoringCaseFlags { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets the state of the sleep mode output
/// </summary>
public sbyte SleepModeOutput { get; init; }
/// <summary>
/// Gets or sets whether a contamination warning is present
/// </summary>
public bool HostErrorFlagContaminationWarning { get; init; }
/// <summary>
/// Gets or sets whether a contamination error is present
/// </summary>
public bool HostErrorFlagContaminationError { get; init; }
/// <summary>
/// Gets or sets whether a manipulation error is present
/// </summary>
public bool HostErrorFlagManipulationError { get; init; }
/// <summary>
/// Gets or sets whether glare is present
/// </summary>
public bool HostErrorFlagGlare { get; init; }
/// <summary>
/// Gets or sets whether a reference contour is intruded
/// </summary>
public bool HostErrorFlagReferenceContourIntruded { get; init; }
/// <summary>
/// Gets or sets whether a critical error is present
/// </summary>
public bool HostErrorFlagCriticalError { get; init; }
/// <summary>
/// Gets or sets the first linear velocity output
/// </summary>
public short Velocity0 { get; init; }
/// <summary>
/// Gets or sets the second linear velocity output
/// </summary>
public short Velocity1 { get; init; }
/// <summary>
/// Gets or sets whether the first linear velocity output is valid
/// </summary>
public bool IsVelocity0Valid { get; init; }
/// <summary>
/// Gets or sets whether the second linear velocity output is valid
/// </summary>
public bool IsVelocity1Valid { get; init; }
/// <summary>
/// Gets or sets whether the first linear velocity output is transmitted safely
/// </summary>
public bool IsVelocity0TransmittedSafely { get; init; }
/// <summary>
/// Gets or sets whether the second linear velocity output is transmitted safely
/// </summary>
public bool IsVelocity1TransmittedSafely { get; init; }
/// <summary>
/// Gets or sets the resulting velocity for each monitoring case table
/// </summary>
public IReadOnlyList<short> ResultingVelocity { get; init; } = Array.Empty<short>();
/// <summary>
/// Gets or sets whether the resulting velocities are valid
/// </summary>
public IReadOnlyList<bool> ResultingVelocityIsValid { get; init; } = Array.Empty<bool>();
/// <summary>
/// Gets or sets whether the sleep mode is valid
/// </summary>
public bool FlagsSleepModeOutputIsValid { get; init; }
/// <summary>
/// Gets or sets whether the error flags are valid
/// </summary>
public bool FlagsHostErrorFlagsAreValid { get; init; }
}

View File

@@ -0,0 +1,150 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Communication settings used to configure the SICK Safety Scanner
/// This structure is used with ChangeCommSettingsCommand to configure scanner parameters
/// </summary>
public sealed class CommSettings
{
/// <summary>
/// Gets or sets the channel number (0-3)
/// </summary>
public byte Channel { get; init; }
/// <summary>
/// Gets or sets the publishing frequency (publish every n-th scan)
/// Example: 1 = publish every scan, 2 = publish every 2nd scan (half frequency)
/// </summary>
public ushort PublishingFrequency { get; init; } = 1;
/// <summary>
/// Gets or sets the interface type
/// 0: EFI-pro, 1: EtherNet/IP, 3: Profinet, 4: Non-safe Ethernet
/// </summary>
public InterfaceType EInterfaceType { get; init; }
/// <summary>
/// Gets or sets the start angle in radians
/// If start and end angles are equal, all angles are regarded
/// </summary>
public double StartAngle { get; init; }
/// <summary>
/// Gets or sets the end angle in radians
/// If start and end angles are equal, all angles are regarded
/// </summary>
public double EndAngle { get; init; }
/// <summary>
/// Gets or sets the enabled features as a bitset (ushort)
/// Use SensorDataFeatures constants or SensorDataFeatures.ToFeatureFlags() to set
/// </summary>
public ushort Features { get; init; } = SensorDataFeatures.All;
/// <summary>
/// Gets or sets whether general system state feature is enabled
/// </summary>
public bool GeneralSystemStateEnabled { get; init; } = true;
/// <summary>
/// Gets or sets whether derived settings feature is enabled
/// </summary>
public bool DerivedSettingsEnabled { get; init; } = true;
/// <summary>
/// Gets or sets whether measurement data feature is enabled
/// </summary>
public bool MeasurementDataEnabled { get; init; } = true;
/// <summary>
/// Gets or sets whether intrusion data feature is enabled
/// </summary>
public bool IntrusionDataEnabled { get; init; } = true;
/// <summary>
/// Gets or sets whether application data feature is enabled
/// </summary>
public bool ApplicationDataEnabled { get; init; } = true;
/// <summary>
/// Gets or sets whether the channel is enabled
/// </summary>
public bool Enabled { get; init; } = true;
/// <summary>
/// Gets or sets the host UDP port (0 = auto-assign)
/// </summary>
public ushort HostUdpPort { get; init; }
/// <summary>
/// Gets or sets the host IP address as a string (e.g., "192.168.1.100")
/// </summary>
public string HostIp { get; init; } = "192.168.1.100";
/// <summary>
/// Creates a CommSettings with all features enabled
/// </summary>
public static CommSettings CreateDefault()
{
return new CommSettings
{
Channel = 0,
PublishingFrequency = 1,
EInterfaceType = InterfaceType.NonSafeEthernet,
StartAngle = 0.0,
EndAngle = 0.0, // Equal to start angle means all angles
Features = SensorDataFeatures.All,
GeneralSystemStateEnabled = true,
DerivedSettingsEnabled = true,
MeasurementDataEnabled = true,
IntrusionDataEnabled = true,
ApplicationDataEnabled = true,
Enabled = true,
HostUdpPort = 0,
HostIp = "192.168.1.100"
};
}
/// <summary>
/// Creates a CommSettings with custom feature flags
/// </summary>
public static CommSettings Create(
byte channel,
string hostIp,
ushort hostUdpPort,
bool generalSystemState = true,
bool derivedSettings = true,
bool measurementData = true,
bool intrusionData = true,
bool applicationData = true,
ushort publishingFrequency = 1,
double startAngle = 0.0,
double endAngle = 0.0,
InterfaceType interfaceType = InterfaceType.NonSafeEthernet,
bool enabled = true)
{
return new CommSettings
{
Channel = channel,
PublishingFrequency = publishingFrequency,
EInterfaceType = interfaceType,
StartAngle = startAngle,
EndAngle = endAngle,
Features = SensorDataFeatures.ToFeatureFlags(
generalSystemState,
derivedSettings,
measurementData,
intrusionData,
applicationData),
GeneralSystemStateEnabled = generalSystemState,
DerivedSettingsEnabled = derivedSettings,
MeasurementDataEnabled = measurementData,
IntrusionDataEnabled = intrusionData,
ApplicationDataEnabled = applicationData,
Enabled = enabled,
HostUdpPort = hostUdpPort,
HostIp = hostIp
};
}
}

View File

@@ -0,0 +1,133 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the configuration data from a COLA2 variable command response
/// Used for current and persistent sensor configuration
/// </summary>
public sealed class ConfigData
{
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets the host IP address
/// </summary>
public string HostIp { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the host UDP port
/// </summary>
public ushort HostUdpPort { get; init; }
/// <summary>
/// Gets or sets the channel number (0-3)
/// </summary>
public byte Channel { get; init; }
/// <summary>
/// Gets or sets whether the channel is enabled
/// </summary>
public bool Enabled { get; init; }
/// <summary>
/// Gets or sets the interface type
/// </summary>
public InterfaceType EInterfaceType { get; init; }
/// <summary>
/// Gets or sets the publishing frequency (publish every n-th scan)
/// </summary>
public ushort PublishingFrequency { get; init; }
/// <summary>
/// Gets or sets the start angle in radians
/// </summary>
public double StartAngle { get; init; }
/// <summary>
/// Gets or sets the end angle in radians
/// </summary>
public double EndAngle { get; init; }
/// <summary>
/// Gets or sets the enabled features (bit flags)
/// Bit 0: GeneralSystemState, Bit 1: DerivedSettings, Bit 2: MeasurementData,
/// Bit 3: IntrusionData, Bit 4: ApplicationData
/// </summary>
public ushort Features { get; init; }
/// <summary>
/// Gets or sets whether general system state feature is enabled
/// </summary>
public bool GeneralSystemStateEnabled { get; init; }
/// <summary>
/// Gets or sets whether derived settings feature is enabled
/// </summary>
public bool DerivedSettingsEnabled { get; init; }
/// <summary>
/// Gets or sets whether measurement data feature is enabled
/// </summary>
public bool MeasurementDataEnabled { get; init; }
/// <summary>
/// Gets or sets whether intrusion data feature is enabled
/// </summary>
public bool IntrusionDataEnabled { get; init; }
/// <summary>
/// Gets or sets whether application data feature is enabled
/// </summary>
public bool ApplicationDataEnabled { get; init; }
// Derived values (similar to DerivedValues structure)
/// <summary>
/// Gets or sets the multiplication factor for beam distances
/// </summary>
public ushort DerivedMultiplicationFactor { get; init; }
/// <summary>
/// Gets or sets the number of beams
/// </summary>
public ushort DerivedNumberOfBeams { get; init; }
/// <summary>
/// Gets or sets the scan time in milliseconds
/// </summary>
public ushort DerivedScanTime { get; init; }
/// <summary>
/// Gets or sets the derived start angle in radians
/// </summary>
public double DerivedStartAngle { get; init; }
/// <summary>
/// Gets or sets the angular beam resolution in radians
/// </summary>
public double DerivedAngularBeamResolution { get; init; }
/// <summary>
/// Gets or sets the interbeam period in microseconds
/// </summary>
public uint DerivedInterbeamPeriod { get; init; }
}

View File

@@ -0,0 +1,63 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the configuration metadata from a COLA2 variable command response
/// </summary>
public sealed class ConfigMetadata
{
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets the modification time date (days since 1980-01-01)
/// </summary>
public ushort ModificationTimeDate { get; init; }
/// <summary>
/// Gets or sets the modification time (milliseconds since midnight)
/// </summary>
public uint ModificationTimeTime { get; init; }
/// <summary>
/// Gets or sets the transfer time date (days since 1980-01-01)
/// </summary>
public ushort TransferTimeDate { get; init; }
/// <summary>
/// Gets or sets the transfer time (milliseconds since midnight)
/// </summary>
public uint TransferTimeTime { get; init; }
/// <summary>
/// Gets or sets the application checksum
/// </summary>
public uint AppChecksum { get; init; }
/// <summary>
/// Gets or sets the overall checksum
/// </summary>
public uint OverallChecksum { get; init; }
/// <summary>
/// Gets or sets the integrity hash (array of uint32 values)
/// </summary>
public IReadOnlyList<uint> IntegrityHash { get; init; } = Array.Empty<uint>();
}

View File

@@ -0,0 +1,117 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the content of the data header of a UDP data packet
/// </summary>
public sealed class DataHeader
{
/// <summary>
/// Gets or sets the version indicator (capital letter 'V' or 'R' for releases)
/// </summary>
public byte VersionIndicator { get; init; }
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajor { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinor { get; init; }
/// <summary>
/// Gets or sets the release of the version
/// </summary>
public byte VersionRelease { get; init; }
/// <summary>
/// Gets or sets the serial number of the device
/// </summary>
public uint SerialNumberOfDevice { get; init; }
/// <summary>
/// Gets or sets the serial number of the system plug
/// </summary>
public uint SerialNumberOfSystemPlug { get; init; }
/// <summary>
/// Gets or sets the channel number (0-3)
/// </summary>
public byte ChannelNumber { get; init; }
/// <summary>
/// Gets or sets the sequence number (increases with each measurement)
/// </summary>
public uint SequenceNumber { get; init; }
/// <summary>
/// Gets or sets the scan number
/// </summary>
public uint ScanNumber { get; init; }
/// <summary>
/// Gets or sets the timestamp date
/// </summary>
public ushort TimestampDate { get; init; }
/// <summary>
/// Gets or sets the timestamp time
/// </summary>
public uint TimestampTime { get; init; }
/// <summary>
/// Gets or sets the general system state block offset
/// </summary>
public ushort GeneralSystemStateBlockOffset { get; init; }
/// <summary>
/// Gets or sets the general system state block size
/// </summary>
public ushort GeneralSystemStateBlockSize { get; init; }
/// <summary>
/// Gets or sets the derived values block offset
/// </summary>
public ushort DerivedValuesBlockOffset { get; init; }
/// <summary>
/// Gets or sets the derived values block size
/// </summary>
public ushort DerivedValuesBlockSize { get; init; }
/// <summary>
/// Gets or sets the measurement data block offset
/// </summary>
public ushort MeasurementDataBlockOffset { get; init; }
/// <summary>
/// Gets or sets the measurement data block size
/// </summary>
public ushort MeasurementDataBlockSize { get; init; }
/// <summary>
/// Gets or sets the intrusion data block offset
/// </summary>
public ushort IntrusionDataBlockOffset { get; init; }
/// <summary>
/// Gets or sets the intrusion data block size
/// </summary>
public ushort IntrusionDataBlockSize { get; init; }
/// <summary>
/// Gets or sets the application data block offset
/// </summary>
public ushort ApplicationDataBlockOffset { get; init; }
/// <summary>
/// Gets or sets the application data block size
/// </summary>
public ushort ApplicationDataBlockSize { get; init; }
/// <summary>
/// Gets whether the data header is empty
/// </summary>
public bool IsEmpty { get; init; }
}

View File

@@ -0,0 +1,53 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the contents of a UDP datagram header
/// Used to match datagrams together to form a complete data packet
/// </summary>
public sealed class DatagramHeader
{
/// <summary>
/// Size of the datagram header (24 bytes)
/// </summary>
public const int HeaderSize = 24;
/// <summary>
/// Gets or sets the datagram marker
/// </summary>
public uint DatagramMarker { get; init; }
/// <summary>
/// Gets or sets the protocol
/// </summary>
public ushort Protocol { get; init; }
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte MajorVersion { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte MinorVersion { get; init; }
/// <summary>
/// Gets or sets the total length of the data packet (excluding headers)
/// Total length of the (possibly fragmented) measurement data instance
/// </summary>
public uint TotalLength { get; init; }
/// <summary>
/// Gets or sets the identification of the data
/// Datagrams (fragments) that belong to the same measurement data output instance share the same identifier
/// The number increases with each measurement data instance generated per channel
/// </summary>
public uint Identification { get; init; }
/// <summary>
/// Gets or sets the fragment offset (in bytes)
/// Offset of the measurement data carried in this datagram (fragment) relative to the start of the overall measurement data output instance
/// </summary>
public uint FragmentOffset { get; init; }
}

View File

@@ -0,0 +1,47 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the derived configuration of the measurement data channel
/// </summary>
public sealed class DerivedValues
{
/// <summary>
/// Angle resolution constant to convert sensor input to the right frame (4194304.0)
/// </summary>
public const double AngleResolution = 4194304.0;
/// <summary>
/// Gets or sets the multiplication factor to be applied to beam distance values to get distance in millimeter
/// </summary>
public ushort MultiplicationFactor { get; init; }
/// <summary>
/// Gets or sets the number of beams of the current scan
/// </summary>
public ushort NumberOfBeams { get; init; }
/// <summary>
/// Gets or sets the time of the scan (ms)
/// </summary>
public ushort ScanTime { get; init; }
/// <summary>
/// Gets or sets the start angle of the scan (radians)
/// </summary>
public double StartAngle { get; init; }
/// <summary>
/// Gets or sets the angular resolution between beams (radians)
/// </summary>
public double AngularBeamResolution { get; init; }
/// <summary>
/// Gets or sets the time between consecutive beams (microseconds)
/// </summary>
public uint InterbeamPeriod { get; init; }
/// <summary>
/// Gets whether derived values are empty (not enabled)
/// </summary>
public bool IsEmpty { get; init; }
}

View File

@@ -0,0 +1,13 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the device name from a COLA2 variable command response
/// </summary>
public sealed class DeviceName
{
/// <summary>
/// Gets or sets the device name string
/// </summary>
public string Name { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,64 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Device status enumeration for SICK Safety Scanner
/// </summary>
public enum SopasDeviceStatus : byte
{
/// <summary>
/// Unknown status
/// </summary>
Unknown = 0,
/// <summary>
/// Start up status
/// </summary>
StartUp = 1,
/// <summary>
/// Service mode
/// </summary>
ServiceMode = 2,
/// <summary>
/// Normal operation
/// </summary>
NormalOperation = 3,
/// <summary>
/// Suspended operation
/// </summary>
SuspendedOperation = 4,
/// <summary>
/// Service recommended
/// </summary>
ServiceRecommended = 5,
/// <summary>
/// Service required
/// </summary>
ServiceRequired = 6,
/// <summary>
/// Recoverable error
/// </summary>
RecoverableError = 7,
/// <summary>
/// Fatal error
/// </summary>
FatalError = 8
}
/// <summary>
/// Contains the device status from a COLA2 variable command response
/// </summary>
public sealed class DeviceStatus
{
/// <summary>
/// Gets or sets the device status value
/// </summary>
public SopasDeviceStatus Status { get; init; }
}

View File

@@ -0,0 +1,99 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains field data for warning and protective fields from a COLA2 variable command response
/// </summary>
public sealed class FieldData
{
/// <summary>
/// Gets or sets whether the field data is valid
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets whether the field data is defined
/// </summary>
public bool IsDefined { get; init; }
/// <summary>
/// Gets or sets the evaluation method
/// </summary>
public byte EvalMethod { get; init; }
/// <summary>
/// Gets or sets the multiple sampling value
/// </summary>
public ushort MultiSampling { get; init; }
/// <summary>
/// Gets or sets the object resolution
/// </summary>
public ushort ObjectResolution { get; init; }
/// <summary>
/// Gets or sets the index of the field set this field belongs to
/// </summary>
public ushort FieldSetIndex { get; init; }
/// <summary>
/// Gets or sets the length of the field name
/// </summary>
public uint NameLength { get; init; }
/// <summary>
/// Gets or sets the field name string
/// </summary>
public string FieldName { get; init; } = string.Empty;
/// <summary>
/// Gets or sets whether this is a warning field
/// </summary>
public bool IsWarningField { get; init; }
/// <summary>
/// Gets or sets whether this is a protective field
/// </summary>
public bool IsProtectiveField { get; init; }
/// <summary>
/// Gets or sets the beam distances vector (in mm)
/// One distance value per beam
/// </summary>
public IReadOnlyList<ushort> BeamDistances { get; init; } = Array.Empty<ushort>();
/// <summary>
/// Gets or sets the start angle in radians
/// </summary>
public double StartAngle { get; init; }
/// <summary>
/// Gets or sets the end angle in radians
/// </summary>
public double EndAngle { get; init; }
/// <summary>
/// Gets or sets the angular beam resolution in radians
/// </summary>
public double AngularBeamResolution { get; init; }
}

View File

@@ -0,0 +1,43 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains field sets data from a COLA2 variable command response
/// </summary>
public sealed class FieldSets
{
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets the length of each field name (one per field set)
/// </summary>
public IReadOnlyList<uint> NameLengths { get; init; } = [];
/// <summary>
/// Gets or sets the field names (one per field set)
/// </summary>
public IReadOnlyList<string> FieldNames { get; init; } = [];
/// <summary>
/// Gets or sets whether each field set is defined
/// </summary>
public IReadOnlyList<bool> IsDefined { get; init; } = [];
}

View File

@@ -0,0 +1,13 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the firmware version from a COLA2 variable command response
/// </summary>
public sealed class FirmwareVersion
{
/// <summary>
/// Gets or sets the firmware version string
/// </summary>
public string Version { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,87 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the general system state including run/standby modes, cut-off paths, monitoring cases, and errors
/// </summary>
public sealed class GeneralSystemState
{
/// <summary>
/// Gets or sets whether run mode is active
/// </summary>
public bool IsRunModeActive { get; init; }
/// <summary>
/// Gets or sets whether standby mode is active
/// </summary>
public bool IsStandbyModeActive { get; init; }
/// <summary>
/// Gets or sets whether a contamination warning exists
/// </summary>
public bool HasContaminationWarning { get; init; }
/// <summary>
/// Gets or sets whether a contamination error exists
/// </summary>
public bool HasContaminationError { get; init; }
/// <summary>
/// Gets or sets whether the reference contour status is true
/// </summary>
public bool ReferenceContourStatus { get; init; }
/// <summary>
/// Gets or sets whether manipulation status is set to true
/// </summary>
public bool ManipulationStatus { get; init; }
/// <summary>
/// Gets or sets the state for all safe cut-off paths
/// </summary>
public IReadOnlyList<bool> SafeCutOffPaths { get; init; } = [];
/// <summary>
/// Gets or sets the state of all non-safe cut-off paths
/// </summary>
public IReadOnlyList<bool> NonSafeCutOffPaths { get; init; } = [];
/// <summary>
/// Gets or sets whether a cut-off path has to be reset
/// </summary>
public IReadOnlyList<bool> ResetRequiredCutOffPaths { get; init; } = [];
/// <summary>
/// Gets or sets the current monitoring case number for table 1
/// </summary>
public byte CurrentMonitoringCaseNoTable1 { get; init; }
/// <summary>
/// Gets or sets the current monitoring case number for table 2
/// </summary>
public byte CurrentMonitoringCaseNoTable2 { get; init; }
/// <summary>
/// Gets or sets the current monitoring case number for table 3
/// </summary>
public byte CurrentMonitoringCaseNoTable3 { get; init; }
/// <summary>
/// Gets or sets the current monitoring case number for table 4
/// </summary>
public byte CurrentMonitoringCaseNoTable4 { get; init; }
/// <summary>
/// Gets or sets whether an application error exists
/// </summary>
public bool HasApplicationError { get; init; }
/// <summary>
/// Gets or sets whether a device error exists
/// </summary>
public bool HasDeviceError { get; init; }
/// <summary>
/// Gets whether general system state is empty (not enabled)
/// </summary>
public bool IsEmpty { get; init; }
}

View File

@@ -0,0 +1,17 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains all intrusion data
/// </summary>
public sealed class IntrusionData
{
/// <summary>
/// Gets or sets all intrusion datums
/// </summary>
public IReadOnlyList<IntrusionDatum> IntrusionDatums { get; init; } = Array.Empty<IntrusionDatum>();
/// <summary>
/// Gets whether intrusion data is empty (not enabled)
/// </summary>
public bool IsEmpty { get; init; }
}

View File

@@ -0,0 +1,17 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Represents a single intrusion datum
/// </summary>
public sealed class IntrusionDatum
{
/// <summary>
/// Gets or sets the size of the flag vector
/// </summary>
public int Size { get; init; }
/// <summary>
/// Gets or sets the flags vector (one flag per beam indicating intrusion)
/// </summary>
public IReadOnlyList<bool> Flags { get; init; } = Array.Empty<bool>();
}

View File

@@ -0,0 +1,29 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains all scan points of a single measurement
/// </summary>
public sealed class MeasurementData
{
/// <summary>
/// Gets the number of beams in this measurement
/// </summary>
public uint NumberOfBeams { get; init; }
/// <summary>
/// Gets all scan points
/// </summary>
public IReadOnlyList<ScanPoint> ScanPoints { get; init; }
/// <summary>
/// Gets whether measurement data is empty (not enabled)
/// </summary>
public bool IsEmpty { get; init; }
public MeasurementData(uint numberOfBeams, IReadOnlyList<ScanPoint> scanPoints, bool isEmpty = false)
{
NumberOfBeams = numberOfBeams;
ScanPoints = scanPoints ?? Array.Empty<ScanPoint>();
IsEmpty = isEmpty;
}
}

View File

@@ -0,0 +1,28 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains monitoring case data from a COLA2 variable command response
/// </summary>
public sealed class MonitoringCaseData
{
/// <summary>
/// Gets or sets whether the monitoring case data is valid
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Gets or sets the monitoring case number
/// </summary>
public ushort MonitoringCaseNumber { get; init; }
/// <summary>
/// Gets or sets the field indices associated with this monitoring case
/// </summary>
public IReadOnlyList<ushort> FieldIndices { get; init; } = Array.Empty<ushort>();
/// <summary>
/// Gets or sets whether each field is configured and valid
/// </summary>
public IReadOnlyList<bool> FieldsValid { get; init; } = Array.Empty<bool>();
}

View File

@@ -0,0 +1,13 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the order number from a COLA2 variable command response
/// </summary>
public sealed class OrderNumber
{
/// <summary>
/// Gets or sets the order number string
/// </summary>
public string Number { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,116 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Packet buffer for COLA2 communication
/// Thread-safe wrapper around byte buffer - matches C++ shared_ptr<vector const> semantics
/// </summary>
public sealed class PacketBuffer : IDisposable
{
private readonly byte[] _buffer;
private readonly int _length;
private bool _disposed;
/// <summary>
/// Maximum size of packet buffer (matches C++ MAXSIZE = 10000)
/// </summary>
public const int MaxSize = 10000;
/// <summary>
/// Creates a new packet buffer from byte array
/// </summary>
public PacketBuffer(byte[] buffer, int length)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (length < 0 || length > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(length));
if (length > MaxSize)
throw new ArgumentException($"Length {length} exceeds MaxSize {MaxSize}", nameof(length));
// Copy buffer to ensure immutability (like C++ shared_ptr<vector const>)
_buffer = new byte[length];
Array.Copy(buffer, 0, _buffer, 0, length);
_length = length;
}
/// <summary>
/// Creates a new packet buffer from ReadOnlyMemory
/// </summary>
public PacketBuffer(ReadOnlyMemory<byte> memory)
{
if (memory.Length > MaxSize)
throw new ArgumentException($"Length {memory.Length} exceeds MaxSize {MaxSize}", nameof(memory));
_buffer = memory.ToArray();
_length = _buffer.Length;
}
/// <summary>
/// Creates a new packet buffer from ReadOnlySpan
/// </summary>
public PacketBuffer(ReadOnlySpan<byte> span)
{
if (span.Length > MaxSize)
throw new ArgumentException($"Length {span.Length} exceeds MaxSize {MaxSize}", nameof(span));
_buffer = span.ToArray();
_length = _buffer.Length;
}
/// <summary>
/// Gets the buffer as ReadOnlyMemory (zero-copy if possible)
/// </summary>
public ReadOnlyMemory<byte> GetBuffer()
{
if (_disposed)
throw new ObjectDisposedException(nameof(PacketBuffer));
return new ReadOnlyMemory<byte>(_buffer, 0, _length);
}
/// <summary>
/// Gets the buffer as ReadOnlySpan (zero-copy)
/// </summary>
public ReadOnlySpan<byte> GetBufferSpan()
{
if (_disposed)
throw new ObjectDisposedException(nameof(PacketBuffer));
return new ReadOnlySpan<byte>(_buffer, 0, _length);
}
/// <summary>
/// Gets the length of the buffer
/// </summary>
public int Length
{
get
{
if (_disposed)
throw new ObjectDisposedException(nameof(PacketBuffer));
return _length;
}
}
/// <summary>
/// Gets a copy of the buffer as byte array
/// </summary>
public byte[] ToArray()
{
if (_disposed)
throw new ObjectDisposedException(nameof(PacketBuffer));
var result = new byte[_length];
Array.Copy(_buffer, 0, result, 0, _length);
return result;
}
public void Dispose()
{
_disposed = true;
}
}

View File

@@ -0,0 +1,37 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Represents a PacketBuffer with a parsed DatagramHeader
/// Used for merging fragmented UDP packets
/// </summary>
public sealed class ParsedPacketBuffer
{
/// <summary>
/// Gets the packet buffer
/// </summary>
public PacketBuffer PacketBuffer { get; init; }
/// <summary>
/// Gets the parsed datagram header
/// </summary>
public DatagramHeader DatagramHeader { get; init; }
public ParsedPacketBuffer(PacketBuffer packetBuffer, DatagramHeader datagramHeader)
{
PacketBuffer = packetBuffer ?? throw new ArgumentNullException(nameof(packetBuffer));
DatagramHeader = datagramHeader ?? throw new ArgumentNullException(nameof(datagramHeader));
}
/// <summary>
/// Compares two ParsedPacketBuffer instances by fragment offset for sorting
/// </summary>
public static int CompareByOffset(ParsedPacketBuffer x, ParsedPacketBuffer y)
{
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;
return x.DatagramHeader.FragmentOffset.CompareTo(y.DatagramHeader.FragmentOffset);
}
}

View File

@@ -0,0 +1,13 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the project name from a COLA2 variable command response
/// </summary>
public sealed class ProjectName
{
/// <summary>
/// Gets or sets the project name string
/// </summary>
public string Name { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,49 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the required user action information from a COLA2 variable command response
/// Provides additional information about the SOPAS state
/// </summary>
public sealed class RequiredUserAction
{
/// <summary>
/// Gets or sets whether the configuration has to be confirmed
/// </summary>
public bool ConfirmConfiguration { get; init; }
/// <summary>
/// Gets or sets whether the configuration has to be checked
/// </summary>
public bool CheckConfiguration { get; init; }
/// <summary>
/// Gets or sets whether the environment has to be checked
/// </summary>
public bool CheckEnvironment { get; init; }
/// <summary>
/// Gets or sets whether the application interfaces have to be checked
/// </summary>
public bool CheckApplicationInterfaces { get; init; }
/// <summary>
/// Gets or sets whether the device has to be checked
/// </summary>
public bool CheckDevice { get; init; }
/// <summary>
/// Gets or sets whether the setup procedure has to be run
/// </summary>
public bool RunSetupProcedure { get; init; }
/// <summary>
/// Gets or sets whether the firmware has to be checked
/// </summary>
public bool CheckFirmware { get; init; }
/// <summary>
/// Gets or sets whether the user has to wait
/// </summary>
public bool Wait { get; init; }
}

View File

@@ -0,0 +1,66 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Represents a single scan point from the SICK Safety Scanner
/// </summary>
public sealed class ScanPoint
{
/// <summary>
/// Gets the angle in sensor coordinates (radians)
/// </summary>
public double Angle { get; init; }
/// <summary>
/// Gets the distance of the measured scanpoint (mm)
/// </summary>
public ushort Distance { get; init; }
/// <summary>
/// Gets the reflectivity value (0-255)
/// </summary>
public byte Reflectivity { get; init; }
/// <summary>
/// Gets whether the scanpoint is valid
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Gets whether the scanpoint is infinite (no object detected)
/// </summary>
public bool IsInfinite { get; init; }
/// <summary>
/// Gets whether there is glare in the scanpoint
/// </summary>
public bool HasGlare { get; init; }
/// <summary>
/// Gets whether the scanpoint detects a reflector
/// </summary>
public bool IsReflector { get; init; }
/// <summary>
/// Gets whether the scanpoint is contaminated
/// </summary>
public bool IsContaminated { get; init; }
/// <summary>
/// Gets whether there is a contamination warning
/// </summary>
public bool HasContaminationWarning { get; init; }
public ScanPoint(double angle, ushort distance, byte reflectivity, bool isValid,
bool isInfinite, bool hasGlare, bool isReflector, bool isContaminated, bool hasContaminationWarning)
{
Angle = angle;
Distance = distance;
Reflectivity = reflectivity;
IsValid = isValid;
IsInfinite = isInfinite;
HasGlare = hasGlare;
IsReflector = isReflector;
IsContaminated = isContaminated;
HasContaminationWarning = hasContaminationWarning;
}
}

View File

@@ -0,0 +1,80 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Sensor data feature flags constants and helper methods
/// Used to configure which data blocks should be included in scan data output
/// </summary>
public static class SensorDataFeatures
{
/// <summary>
/// All features enabled (0b11111)
/// </summary>
public const ushort All = 0b11111;
/// <summary>
/// No features enabled
/// </summary>
public const ushort None = 0;
/// <summary>
/// General system state feature (bit 0)
/// </summary>
public const ushort GeneralSystemState = 1 << 0;
/// <summary>
/// Derived settings feature (bit 1)
/// </summary>
public const ushort DerivedSettings = 1 << 1;
/// <summary>
/// Measurement data feature (bit 2)
/// </summary>
public const ushort MeasurementData = 1 << 2;
/// <summary>
/// Intrusion data feature (bit 3)
/// </summary>
public const ushort IntrusionData = 1 << 3;
/// <summary>
/// Application data feature (bit 4)
/// </summary>
public const ushort ApplicationData = 1 << 4;
/// <summary>
/// Checks if a certain feature flag is set in the bitset
/// </summary>
/// <param name="bitset">The bitset expressed as ushort</param>
/// <param name="flag">The feature flag to check</param>
/// <returns>True if the flag is set, false otherwise</returns>
public static bool IsFlagSet(ushort bitset, ushort flag)
{
return (bitset & flag) == flag;
}
/// <summary>
/// Converts boolean indicators (for the sensor features to be streamed) into a bitset
/// </summary>
/// <param name="generalSystemState">Indicator for the general system state channel</param>
/// <param name="derivedSettings">Turn on/off derived values</param>
/// <param name="measurementData">Turn on/off measurement data</param>
/// <param name="intrusionData">Turn on/off safety field intrusion data</param>
/// <param name="applicationData">Turn on/off application data</param>
/// <returns>A bitset containing indication flags for each feature channel</returns>
public static ushort ToFeatureFlags(
bool generalSystemState,
bool derivedSettings,
bool measurementData,
bool intrusionData,
bool applicationData)
{
return (ushort)(
(generalSystemState ? GeneralSystemState : 0) +
(derivedSettings ? DerivedSettings : 0) +
(measurementData ? MeasurementData : 0) +
(intrusionData ? IntrusionData : 0) +
(applicationData ? ApplicationData : 0)
);
}
}

View File

@@ -0,0 +1,13 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the serial number from a COLA2 variable command response
/// </summary>
public sealed class SerialNumber
{
/// <summary>
/// Gets or sets the serial number string
/// </summary>
public string Number { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,186 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Device state enumeration for SICK Safety Scanner
/// </summary>
public enum DeviceState : byte
{
/// <summary>
/// Normal state
/// </summary>
Normal = 0,
/// <summary>
/// Error state
/// </summary>
Error = 1,
/// <summary>
/// Initialization state
/// </summary>
Initialization = 2,
/// <summary>
/// Shutdown state
/// </summary>
Shutdown = 3,
/// <summary>
/// Optics cover calibration state
/// </summary>
OpticsCoverCalibration = 4
}
/// <summary>
/// Config state enumeration for SICK Safety Scanner
/// </summary>
public enum ConfigState : byte
{
/// <summary>
/// Unknown config state
/// </summary>
Unknown = 0,
/// <summary>
/// Config required
/// </summary>
ConfigRequired = 1,
/// <summary>
/// Config in progress
/// </summary>
ConfigInProgress = 2,
/// <summary>
/// Not verified
/// </summary>
NotVerified = 3,
/// <summary>
/// Rejected
/// </summary>
Rejected = 4,
/// <summary>
/// Verified
/// </summary>
Verified = 5,
/// <summary>
/// Internal error
/// </summary>
InternalError = 6,
/// <summary>
/// Verification in progress
/// </summary>
VerificationInProgress = 7
}
/// <summary>
/// Application state enumeration for SICK Safety Scanner
/// </summary>
public enum ApplicationState : byte
{
/// <summary>
/// Application stopped
/// </summary>
Stopped = 0,
/// <summary>
/// Application starting
/// </summary>
Starting = 1,
/// <summary>
/// Waiting for partners
/// </summary>
WaitingForPartners = 2,
/// <summary>
/// Waiting for inputs
/// </summary>
WaitingForInputs = 3,
/// <summary>
/// Application started
/// </summary>
Started = 4,
/// <summary>
/// Sleep mode
/// </summary>
SleepMode = 5
}
/// <summary>
/// Contains the status overview from a COLA2 variable command response
/// </summary>
public sealed class StatusOverview
{
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets the device state
/// </summary>
public DeviceState DeviceState { get; init; }
/// <summary>
/// Gets or sets the config state
/// </summary>
public ConfigState ConfigState { get; init; }
/// <summary>
/// Gets or sets the application state
/// </summary>
public ApplicationState ApplicationState { get; init; }
/// <summary>
/// Gets or sets the current time power on count
/// </summary>
public uint CurrentTimePowerOnCount { get; init; }
/// <summary>
/// Gets or sets the current time (milliseconds since midnight)
/// </summary>
public uint CurrentTimeTime { get; init; }
/// <summary>
/// Gets or sets the current date (days since 1980-01-01)
/// </summary>
public ushort CurrentTimeDate { get; init; }
/// <summary>
/// Gets or sets the error info code
/// </summary>
public uint ErrorInfoCode { get; init; }
/// <summary>
/// Gets or sets the error info time (milliseconds since midnight)
/// </summary>
public uint ErrorInfoTime { get; init; }
/// <summary>
/// Gets or sets the error info date (days since 1980-01-01)
/// </summary>
public ushort ErrorInfoDate { get; init; }
}

View File

@@ -0,0 +1,65 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Interface type enumeration for SICK Safety Scanner
/// </summary>
public enum InterfaceType : byte
{
/// <summary>
/// EFI-pro interface
/// </summary>
EfiPro = 0,
/// <summary>
/// EtherNet/IP interface
/// </summary>
EthernetIp = 1,
/// <summary>
/// Profinet interface
/// </summary>
Profinet = 3,
/// <summary>
/// Non-safe Ethernet interface
/// </summary>
NonSafeEthernet = 4
}
/// <summary>
/// Range enumeration for SICK Safety Scanner
/// </summary>
public enum RangeType
{
/// <summary>
/// Normal range (40m)
/// </summary>
NormalRange = 40,
/// <summary>
/// Long range (64m)
/// </summary>
LongRange = 64
}
/// <summary>
/// Contains the type code from a COLA2 variable command response
/// </summary>
public sealed class TypeCode
{
/// <summary>
/// Gets or sets the type code string
/// </summary>
public string Code { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the interface type
/// </summary>
public InterfaceType InterfaceType { get; init; }
/// <summary>
/// Gets or sets the maximum range in meters
/// </summary>
public double MaxRange { get; init; }
}

View File

@@ -0,0 +1,141 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains all data blocks from a UDP scan data packet
/// </summary>
public sealed class UdpScanData
{
/// <summary>
/// Gets or sets the data header
/// </summary>
public DataHeader Header { get; init; } = new DataHeader();
/// <summary>
/// Gets or sets the general system state
/// </summary>
public GeneralSystemState? GeneralSystemState { get; init; }
/// <summary>
/// Gets or sets the derived values (configuration of data output)
/// </summary>
public DerivedValues? DerivedValues { get; init; }
/// <summary>
/// Gets or sets the measurement data (scan points)
/// </summary>
public MeasurementData? MeasurementData { get; init; }
/// <summary>
/// Gets or sets the intrusion data (field interruption)
/// </summary>
public IntrusionData? IntrusionData { get; init; }
/// <summary>
/// Gets or sets the application data (inputs and outputs)
/// </summary>
public ApplicationData? ApplicationData { get; init; }
/// <summary>
/// Gets the timestamp from the header as DateTime (if available)
/// Returns UTC DateTime to ensure consistency with Cartographer timestamp handling
///
/// SICK Timestamp Format (according to official documentation):
/// - TimestampDate (ushort, offset 24):
/// * If UTC server is time master: Days since 1972-01-01
/// * If Safety Designer device is time master: Number of full 24-hour cycles of time master
/// * If no time sync: Number of full 24-hour cycles since device was switched on
/// - TimestampTime (uint, offset 28): Milliseconds since midnight (or start of 24-hour cycle)
///
/// Since we cannot detect time sync status from UDP packet, we try both base dates:
/// 1. Try 1972-01-01 (UTC server case) - most common in production
/// 2. If result is unreasonable, try 1980-01-01 (legacy/fallback)
/// 3. If TimestampDate = 0: Use current date as base
/// </summary>
public DateTime? Timestamp
{
get
{
if (Header.TimestampDate == 0 && Header.TimestampTime == 0)
return null;
DateTime baseDate;
string baseDateSource = "unknown";
if (Header.TimestampDate == 0)
{
// Date not set on device - use current date as base
var now = DateTime.UtcNow;
baseDate = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
baseDateSource = "current_date";
}
else
{
// Try 1972-01-01 first (UTC server case - most common according to documentation)
var baseDate1972 = new DateTime(1972, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var time1972 = baseDate1972.AddDays(Header.TimestampDate).AddMilliseconds(Header.TimestampTime);
// Check if timestamp with 1972 base is reasonable (within 1 day of now)
var currentTimeTicks = DateTime.UtcNow.Ticks;
var time1972Ticks = time1972.Ticks;
var diff1972Ms = Math.Abs(time1972Ticks - currentTimeTicks) / TimeSpan.TicksPerMillisecond;
if (diff1972Ms <= 86400000) // Within 1 day - reasonable
{
baseDate = baseDate1972;
baseDateSource = "1972-01-01 (UTC server)";
}
else
{
// Try 1980-01-01 as fallback (legacy format or no UTC sync)
var baseDate1980 = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var time1980 = baseDate1980.AddDays(Header.TimestampDate).AddMilliseconds(Header.TimestampTime);
var time1980Ticks = time1980.Ticks;
var diff1980Ms = Math.Abs(time1980Ticks - currentTimeTicks) / TimeSpan.TicksPerMillisecond;
if (diff1980Ms <= diff1972Ms) // 1980 is closer to now
{
baseDate = baseDate1980;
baseDateSource = "1980-01-01 (legacy/fallback)";
}
else
{
// 1972 is closer, use it even if not perfect
baseDate = baseDate1972;
baseDateSource = "1972-01-01 (best match)";
}
}
}
// Add days and milliseconds to base date
var time = baseDate.AddDays(Header.TimestampDate).AddMilliseconds(Header.TimestampTime);
// Validate: timestamp should not be too far in the past or future
var nowTicks = DateTime.UtcNow.Ticks;
var timeTicks = time.Ticks;
var diffMs = Math.Abs(timeTicks - nowTicks) / TimeSpan.TicksPerMillisecond;
// Debug logging for timestamp calculation
if (diffMs > 3600000) // Log if more than 1 hour off
{
var timestampDateStr = Header.TimestampDate == 0 ? "0 (using current date)" : Header.TimestampDate.ToString();
var timestampTimeStr = $"{Header.TimestampTime}ms ({Header.TimestampTime / 3600000.0:F2} hours)";
var calculatedTime = time.ToString("yyyy-MM-dd HH:mm:ss.fff");
System.Diagnostics.Debug.WriteLine(
$"[SICK Timestamp Debug] TimestampDate={timestampDateStr}, TimestampTime={timestampTimeStr}, " +
$"BaseDateSource={baseDateSource}, CalculatedTime={calculatedTime}, " +
$"DiffFromNow={diffMs / 3600000.0:F2} hours");
}
// If timestamp is more than 1 day off, it's likely incorrect
// This can happen if device date is not set correctly or wrong base date
if (diffMs > 86400000) // 1 day in milliseconds
{
// Use current time instead of potentially incorrect device timestamp
return DateTime.UtcNow;
}
// Ensure the result is marked as UTC
return DateTime.SpecifyKind(time, DateTimeKind.Utc);
}
}
}

View File

@@ -0,0 +1,24 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// EventArgs for UdpScanDataReceived event
/// </summary>
public sealed class UdpScanDataEventArgs : EventArgs
{
/// <summary>
/// Gets the timestamp when scan data was received
/// </summary>
public DateTime Timestamp { get; init; }
/// <summary>
/// Gets the parsed UDP scan data
/// </summary>
public UdpScanData ScanData { get; init; }
public UdpScanDataEventArgs(DateTime timestamp, UdpScanData scanData)
{
Timestamp = timestamp;
ScanData = scanData ?? throw new ArgumentNullException(nameof(scanData));
}
}

View File

@@ -0,0 +1,38 @@
namespace Sick.SafetyScanners.DataStructures;
/// <summary>
/// Contains the user name from a COLA2 variable command response
/// </summary>
public sealed class UserName
{
/// <summary>
/// Gets or sets the version indicator (e.g., "V" for version)
/// </summary>
public string VersionCVersion { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the major version number
/// </summary>
public byte VersionMajorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the minor version number
/// </summary>
public byte VersionMinorVersionNumber { get; init; }
/// <summary>
/// Gets or sets the version release number
/// </summary>
public byte VersionReleaseNumber { get; init; }
/// <summary>
/// Gets or sets the length of the user name
/// </summary>
public uint NameLength { get; init; }
/// <summary>
/// Gets or sets the user name string
/// </summary>
public string Name { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,173 @@
namespace Sick.SafetyScanners.Exceptions;
/// <summary>
/// Base exception cho tất cả COLA2 errors
/// </summary>
public class Cola2Exception : Exception
{
public ushort? ErrorCode { get; }
public uint? SessionId { get; }
public ushort? RequestId { get; }
public Cola2Exception(string message) : base(message)
{
}
public Cola2Exception(string message, Exception innerException) : base(message, innerException)
{
}
public Cola2Exception(ushort errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}
public Cola2Exception(uint sessionId, ushort requestId, ushort errorCode, string message)
: base(message)
{
SessionId = sessionId;
RequestId = requestId;
ErrorCode = errorCode;
}
public Cola2Exception(uint sessionId, string message)
: base(message)
{
SessionId = sessionId;
}
}
/// <summary>
/// Session management errors
/// </summary>
public class SessionException : Cola2Exception
{
public SessionException(string message) : base(message)
{
}
public SessionException(string message, Exception innerException) : base(message, innerException)
{
}
public SessionException(uint sessionId, string message) : base(sessionId, message)
{
}
}
/// <summary>
/// Command execution errors
/// </summary>
public class CommandException : Cola2Exception
{
public byte CommandType { get; }
public byte CommandMode { get; }
public CommandException(string message) : base(message)
{
}
public CommandException(byte commandType, byte commandMode, string message) : base(message)
{
CommandType = commandType;
CommandMode = commandMode;
}
public CommandException(uint sessionId, ushort requestId, byte commandType, byte commandMode,
ushort errorCode, string message) : base(sessionId, requestId, errorCode, message)
{
CommandType = commandType;
CommandMode = commandMode;
}
public override string Message =>
$"Command Error (Type: 0x{CommandType:X2}, Mode: 0x{CommandMode:X2}): {base.Message}";
}
/// <summary>
/// Timeout errors
/// </summary>
public class Cola2TimeoutException : Cola2Exception
{
public TimeSpan Timeout { get; }
public string Operation { get; }
public Cola2TimeoutException(string operation, TimeSpan timeout, string message) : base(message)
{
Operation = operation;
Timeout = timeout;
}
public override string Message =>
$"Timeout after {Timeout.TotalMilliseconds}ms during {Operation}: {base.Message}";
}
/// <summary>
/// TCP communication errors
/// </summary>
public class TcpCommunicationException : Cola2Exception
{
public string? ServerIp { get; }
public ushort? ServerPort { get; }
public TcpCommunicationException(string message) : base(message)
{
}
public TcpCommunicationException(string message, Exception innerException)
: base(message, innerException)
{
}
public TcpCommunicationException(string serverIp, ushort serverPort, string message)
: base(message)
{
ServerIp = serverIp;
ServerPort = serverPort;
}
public TcpCommunicationException(string serverIp, ushort serverPort, string message,
Exception innerException) : base(message, innerException)
{
ServerIp = serverIp;
ServerPort = serverPort;
}
public override string Message =>
ServerIp != null && ServerPort.HasValue
? $"TCP Communication Error to {ServerIp}:{ServerPort}: {base.Message}"
: $"TCP Communication Error: {base.Message}";
}
/// <summary>
/// Packet parsing errors
/// </summary>
public class PacketParsingException : Cola2Exception
{
public PacketParsingException(string message) : base(message)
{
}
public PacketParsingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
/// <summary>
/// UDP communication errors
/// </summary>
public class UdpCommunicationException : Cola2Exception
{
public UdpCommunicationException(string message) : base(message)
{
}
public UdpCommunicationException(string message, Exception innerException)
: base(message, innerException)
{
}
public override string Message => $"UDP Communication Error: {base.Message}";
}

View File

@@ -0,0 +1,330 @@
namespace Sick.SafetyScanners.Helpers;
/// <summary>
/// Helper functions for reading and writing data with Big/Little Endian support
/// Matches the logic from C++ ReadWriteHelper.hpp exactly
/// Pure implementation without external dependencies
/// </summary>
public static class ReadWriteHelper
{
/// <summary>
/// Safely gets a byte from ReadOnlySpan, returns 0 if out of range
/// </summary>
private static byte GetByte(ReadOnlySpan<byte> span, int offset)
{
return (offset < 0 || offset >= span.Length) ? (byte)0 : span[offset];
}
/// <summary>
/// Safely gets a byte from Span, returns 0 if out of range
/// </summary>
private static byte GetByte(Span<byte> span, int offset)
{
return (offset < 0 || offset >= span.Length) ? (byte)0 : span[offset];
}
#region Write Operations - No Endian (8-bit only)
/// <summary>
/// Writes an unsigned 8-bit integer to a buffer at offset (no endianness for 1 byte)
/// Matches: writeUint8 in C++ - *(it + 0) = v;
/// </summary>
public static void WriteUint8(Span<byte> span, int offset, byte value)
{
if (offset >= 0 && offset < span.Length)
{
span[offset] = value;
}
}
/// <summary>
/// Writes a signed 8-bit integer to a buffer at offset (no endianness for 1 byte)
/// Matches: writeInt8 in C++ - writeUint8(it, v);
/// </summary>
public static void WriteInt8(Span<byte> span, int offset, sbyte value)
{
WriteUint8(span, offset, unchecked((byte)value));
}
#endregion
#region Write Operations - Big Endian
/// <summary>
/// Writes an unsigned 8-bit integer in big endian encoding
/// Matches: writeUint8BigEndian in C++ - writeUint8(it, v);
/// </summary>
public static void WriteUint8BigEndian(Span<byte> span, int offset, byte value)
{
WriteUint8(span, offset, value);
}
/// <summary>
/// Writes a signed 8-bit integer in big endian encoding
/// Matches: writeInt8BigEndian in C++ - writeInt8(it, v);
/// </summary>
public static void WriteInt8BigEndian(Span<byte> span, int offset, sbyte value)
{
WriteInt8(span, offset, value);
}
/// <summary>
/// Writes an unsigned 16-bit integer in big endian encoding
/// Matches: writeUint16BigEndian in C++ - *(it + 0) = (v & 0xff00) >> 8; *(it + 1) = v & 0xff;
/// </summary>
public static void WriteUint16BigEndian(Span<byte> span, int offset, ushort value)
{
if (offset >= 0 && offset + 1 < span.Length)
{
span[offset + 0] = (byte)((value & 0xff00) >> 8);
span[offset + 1] = (byte)(value & 0xff);
}
}
/// <summary>
/// Writes an unsigned 32-bit integer in big endian encoding
/// Matches: writeUint32BigEndian in C++ - *(it + 0) = (v & 0xff000000) >> 24; ...
/// </summary>
public static void WriteUint32BigEndian(Span<byte> span, int offset, uint value)
{
if (offset >= 0 && offset + 3 < span.Length)
{
span[offset + 0] = (byte)((value & 0xff000000) >> 24);
span[offset + 1] = (byte)((value & 0xff0000) >> 16);
span[offset + 2] = (byte)((value & 0xff00) >> 8);
span[offset + 3] = (byte)(value & 0xff);
}
}
/// <summary>
/// Writes a signed 16-bit integer in big endian encoding
/// Matches: readInt16BigEndian calls readUint16BigEndian in C++
/// </summary>
public static void WriteInt16BigEndian(Span<byte> span, int offset, short value)
{
WriteUint16BigEndian(span, offset, unchecked((ushort)value));
}
/// <summary>
/// Writes a signed 32-bit integer in big endian encoding
/// Matches: readInt32BigEndian calls readUint32BigEndian in C++
/// </summary>
public static void WriteInt32BigEndian(Span<byte> span, int offset, int value)
{
WriteUint32BigEndian(span, offset, unchecked((uint)value));
}
#endregion
#region Write Operations - Little Endian
/// <summary>
/// Writes an unsigned 8-bit integer in little endian encoding
/// Matches: writeUint8LittleEndian in C++ - writeUint8(it, v);
/// </summary>
public static void WriteUint8LittleEndian(Span<byte> span, int offset, byte value)
{
WriteUint8(span, offset, value);
}
/// <summary>
/// Writes a signed 8-bit integer in little endian encoding
/// Matches: writeInt8LittleEndian in C++ - writeInt8(it, v);
/// </summary>
public static void WriteInt8LittleEndian(Span<byte> span, int offset, sbyte value)
{
WriteInt8(span, offset, value);
}
/// <summary>
/// Writes an unsigned 16-bit integer in little endian encoding
/// Matches: writeUint16LittleEndian in C++ - *(it + 0) = v & 0xff; *(it + 1) = (v & 0xff00) >> 8;
/// </summary>
public static void WriteUint16LittleEndian(Span<byte> span, int offset, ushort value)
{
if (offset >= 0 && offset + 1 < span.Length)
{
span[offset + 0] = (byte)(value & 0xff);
span[offset + 1] = (byte)((value & 0xff00) >> 8);
}
}
/// <summary>
/// Writes an unsigned 32-bit integer in little endian encoding
/// Matches: writeUint32LittleEndian in C++ - *(it + 3) = (v & 0xff000000) >> 24; ...
/// </summary>
public static void WriteUint32LittleEndian(Span<byte> span, int offset, uint value)
{
if (offset >= 0 && offset + 3 < span.Length)
{
span[offset + 3] = (byte)((value & 0xff000000) >> 24);
span[offset + 2] = (byte)((value & 0xff0000) >> 16);
span[offset + 1] = (byte)((value & 0xff00) >> 8);
span[offset + 0] = (byte)(value & 0xff);
}
}
/// <summary>
/// Writes a signed 16-bit integer in little endian encoding
/// Matches: readInt16LittleEndian calls readUint16LittleEndian in C++
/// </summary>
public static void WriteInt16LittleEndian(Span<byte> span, int offset, short value)
{
WriteUint16LittleEndian(span, offset, unchecked((ushort)value));
}
/// <summary>
/// Writes a signed 32-bit integer in little endian encoding
/// Matches: writeInt32LittleEndian in C++ - *(it + 3) = (v & 0xff000000) >> 24; ...
/// </summary>
public static void WriteInt32LittleEndian(Span<byte> span, int offset, int value)
{
if (offset >= 0 && offset + 3 < span.Length)
{
span[offset + 3] = (byte)((unchecked((uint)value) & 0xff000000) >> 24);
span[offset + 2] = (byte)((unchecked((uint)value) & 0xff0000) >> 16);
span[offset + 1] = (byte)((unchecked((uint)value) & 0xff00) >> 8);
span[offset + 0] = (byte)(unchecked((uint)value) & 0xff);
}
}
#endregion
#region Read Operations - No Endian (8-bit only)
/// <summary>
/// Reads an unsigned 8-bit integer at offset (no endianness for 1 byte)
/// Matches: readUint8 in C++ - return *(it + 0);
/// </summary>
public static byte ReadUint8(ReadOnlySpan<byte> span, int offset)
{
return GetByte(span, offset);
}
/// <summary>
/// Reads a signed 8-bit integer at offset (no endianness for 1 byte)
/// Matches: readInt8 in C++ - return readUint8(it);
/// </summary>
public static sbyte ReadInt8(ReadOnlySpan<byte> span, int offset)
{
return unchecked((sbyte)ReadUint8(span, offset));
}
#endregion
#region Read Operations - Big Endian
/// <summary>
/// Reads an unsigned 8-bit integer in big endian encoding
/// Matches: readUint8BigEndian in C++ - return readUint8(it);
/// </summary>
public static byte ReadUint8BigEndian(ReadOnlySpan<byte> span, int offset)
{
return ReadUint8(span, offset);
}
/// <summary>
/// Reads a signed 8-bit integer in big endian encoding
/// Matches: readInt8BigEndian in C++ - return readInt8(it);
/// </summary>
public static sbyte ReadInt8BigEndian(ReadOnlySpan<byte> span, int offset)
{
return ReadInt8(span, offset);
}
/// <summary>
/// Reads an unsigned 16-bit integer in big endian encoding
/// Matches: readUint16BigEndian in C++ - return (*(it + 0) << 8) + *(it + 1);
/// </summary>
public static ushort ReadUint16BigEndian(ReadOnlySpan<byte> span, int offset)
{
return (ushort)((GetByte(span, offset + 0) << 8) + GetByte(span, offset + 1));
}
/// <summary>
/// Reads an unsigned 32-bit integer in big endian encoding
/// Matches: readUint32BigEndian in C++ - return (*(it + 0) << 24) + (*(it + 1) << 16) + (*(it + 2) << 8) + *(it + 3);
/// </summary>
public static uint ReadUint32BigEndian(ReadOnlySpan<byte> span, int offset)
{
return ((uint)GetByte(span, offset + 0) << 24) + ((uint)GetByte(span, offset + 1) << 16) + ((uint)GetByte(span, offset + 2) << 8) + GetByte(span, offset + 3);
}
/// <summary>
/// Reads a signed 16-bit integer in big endian encoding
/// Matches: readInt16BigEndian in C++ - return readUint16BigEndian(it);
/// </summary>
public static short ReadInt16BigEndian(ReadOnlySpan<byte> span, int offset)
{
return unchecked((short)ReadUint16BigEndian(span, offset));
}
/// <summary>
/// Reads a signed 32-bit integer in big endian encoding
/// Matches: readInt32BigEndian in C++ - return readUint32BigEndian(it);
/// </summary>
public static int ReadInt32BigEndian(ReadOnlySpan<byte> span, int offset)
{
return unchecked((int)ReadUint32BigEndian(span, offset));
}
#endregion
#region Read Operations - Little Endian
/// <summary>
/// Reads an unsigned 8-bit integer in little endian encoding
/// Matches: readUint8LittleEndian in C++ - return readUint8(it);
/// </summary>
public static byte ReadUint8LittleEndian(ReadOnlySpan<byte> span, int offset)
{
return ReadUint8(span, offset);
}
/// <summary>
/// Reads a signed 8-bit integer in little endian encoding
/// Matches: readInt8LittleEndian in C++ - return readInt8(it);
/// </summary>
public static sbyte ReadInt8LittleEndian(ReadOnlySpan<byte> span, int offset)
{
return ReadInt8(span, offset);
}
/// <summary>
/// Reads an unsigned 16-bit integer in little endian encoding
/// Matches: readUint16LittleEndian in C++ - return (*(it + 1) << 8) + *(it + 0);
/// </summary>
public static ushort ReadUint16LittleEndian(ReadOnlySpan<byte> span, int offset)
{
return (ushort)((GetByte(span, offset + 1) << 8) + GetByte(span, offset + 0));
}
/// <summary>
/// Reads an unsigned 32-bit integer in little endian encoding
/// Matches: readUint32LittleEndian in C++ - return (*(it + 3) << 24) + (*(it + 2) << 16) + (*(it + 1) << 8) + *(it + 0);
/// </summary>
public static uint ReadUint32LittleEndian(ReadOnlySpan<byte> span, int offset)
{
return ((uint)GetByte(span, offset + 3) << 24) + ((uint)GetByte(span, offset + 2) << 16) + ((uint)GetByte(span, offset + 1) << 8) + GetByte(span, offset + 0);
}
/// <summary>
/// Reads a signed 16-bit integer in little endian encoding
/// Matches: readInt16LittleEndian in C++ - return readUint16LittleEndian(it);
/// </summary>
public static short ReadInt16LittleEndian(ReadOnlySpan<byte> span, int offset)
{
return unchecked((short)ReadUint16LittleEndian(span, offset));
}
/// <summary>
/// Reads a signed 32-bit integer in little endian encoding
/// Matches: readInt32LittleEndian in C++ - return readUint32LittleEndian(it);
/// </summary>
public static int ReadInt32LittleEndian(ReadOnlySpan<byte> span, int offset)
{
return unchecked((int)ReadUint32LittleEndian(span, offset));
}
#endregion
}

View File

@@ -0,0 +1,49 @@
namespace Sick.SafetyScanners.Cola2.Commands;
/// <summary>
/// Base interface for all COLA2 commands
/// </summary>
public interface ICola2Command
{
/// <summary>
/// Command type (e.g., 'R' for Read, 'W' for Write, 'O' for Method)
/// </summary>
byte CommandType { get; }
/// <summary>
/// Command mode (e.g., 'I' for Index, 'N' for Name)
/// </summary>
byte CommandMode { get; }
/// <summary>
/// Session ID (set by Cola2Session)
/// </summary>
uint SessionId { get; set; }
/// <summary>
/// Request ID (set by Cola2Session)
/// </summary>
ushort RequestId { get; set; }
/// <summary>
/// Indicates if the command was successfully executed
/// </summary>
bool WasSuccessful { get; }
/// <summary>
/// Gets the data vector for the command payload
/// </summary>
ReadOnlyMemory<byte> GetDataVector();
/// <summary>
/// Processes the reply from the sensor
/// </summary>
/// <param name="replyData">The reply data from the sensor</param>
bool ProcessReply(ReadOnlyMemory<byte> replyData, byte replyCommandType, byte replyCommandMode);
/// <summary>
/// Indicates if the command can be executed without a session ID
/// </summary>
bool CanBeExecutedWithoutSessionId { get; }
}

View File

@@ -0,0 +1,43 @@
using Sick.SafetyScanners.Cola2.Commands;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners.Interfaces;
/// <summary>
/// Interface cho COLA2 session management
/// </summary>
public interface ICola2Session : IDisposable
{
/// <summary>
/// Gets the current session ID, if available
/// </summary>
uint? SessionId { get; }
/// <summary>
/// Indicates whether a COLA2 session is currently opened
/// </summary>
bool IsOpen { get; }
/// <summary>
/// Opens a COLA2 session
/// </summary>
void Open();
/// <summary>
/// Closes the current COLA2 session
/// </summary>
void Close();
/// <summary>
/// Sends a COLA2 command to the sensor and waits for response
/// </summary>
/// <param name="command">The command to send</param>
/// <param name="timeout">The timeout for the operation</param>
void SendCommand(ICola2Command command, TimeDuration? timeout = null);
/// <summary>
/// Gets the next request ID (thread-safe, auto-increments)
/// </summary>
ushort GetNextRequestId();
}

View File

@@ -0,0 +1,50 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners.Interfaces;
/// <summary>
/// Interface cho TCP client communication
/// </summary>
public interface ITcpClient : IDisposable
{
/// <summary>
/// Server IP address
/// </summary>
IpAddress ServerIp { get; }
/// <summary>
/// Server port
/// </summary>
Port ServerPort { get; }
/// <summary>
/// Indicates whether the TCP socket is currently connected
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Establishes a connection to the sensor
/// </summary>
/// <param name="timeout">Timeout for connection</param>
void Connect(TimeDuration? timeout = null);
/// <summary>
/// Disconnects from the sensor
/// </summary>
void Disconnect();
/// <summary>
/// Sends data to the sensor
/// </summary>
/// <param name="data">Data to send</param>
void Send(ReadOnlyMemory<byte> data);
/// <summary>
/// Receives data from the sensor
/// </summary>
/// <param name="timeout">Timeout for receive operation</param>
/// <returns>Received packet buffer</returns>
PacketBuffer Receive(TimeDuration? timeout = null);
}

View File

@@ -0,0 +1,42 @@
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners.Interfaces;
/// <summary>
/// Interface for UDP receiver (not a connection - UDP is connectionless)
/// Used for receiving scan data packets from SICK Safety Scanner via UDP (push mode)
/// Note: This is NOT a "connection" - it's just a UDP socket receiver that binds to a local port
/// and waits for packets from the scanner
/// </summary>
public interface IUdpClient : IDisposable
{
/// <summary>
/// Indicates whether the UDP socket is open (ready to receive)
/// Note: UDP is connectionless, so this just checks if socket is bound/open
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Gets the local port number assigned to this client
/// Returns null if socket is not bound yet
/// </summary>
Port? LocalPort { get; }
/// <summary>
/// Indicates whether data is available in the receiving buffer
/// </summary>
bool IsDataAvailable { get; }
/// <summary>
/// Starts receiving UDP packets on a dedicated high-priority thread
/// </summary>
/// <param name="packetHandler">Callback function to handle received packets</param>
void StartReceiving(Action<PacketBuffer> packetHandler);
/// <summary>
/// Stops the receiving thread
/// </summary>
void Stop();
}

View File

@@ -0,0 +1,711 @@
# Sick.SafetyScanners - COLA2 Communication Library
Thư viện giao tiếp COLA2 cho SICK Safety Scanners được viết bằng C#, chuyển đổi từ project C++ `sick_safetyscanners_base`. Thư viện hỗ trợ đầy đủ giao tiếp với SICK Safety Scanner theo format COLA2.
## 📋 Mục lục
- [Tính năng](#tính-năng)
- [Trạng thái hoàn thiện](#trạng-thái-hoàn-thiện)
- [Cấu trúc Project](#cấu-trúc-project)
- [Hướng dẫn sử dụng](#hướng-dẫn-sử-dụng)
- [API Reference](#api-reference)
- [Thread Safety](#thread-safety)
- [Error Handling](#error-handling)
- [Yêu cầu hệ thống](#yêu-cầu-hệ-thống)
- [License](#license)
## ✨ Tính năng
-**Thread-safe**: Tất cả các operations đều thread-safe
-**Async/Await**: Sử dụng async/await pattern hiện đại
-**COLA2 Protocol**: Hỗ trợ đầy đủ protocol COLA2 cho SICK Safety Scanners
-**Session Management**: Quản lý session tự động
-**Error Handling**: Xử lý lỗi chi tiết với custom exceptions
-**Memory Efficient**: Sử dụng Span/Memory để tối ưu memory
-**UDP Streaming**: Hỗ trợ nhận scan data tự động qua UDP
-**TCP COLA2**: Hỗ trợ gửi lệnh và nhận dữ liệu qua TCP/COLA2
-**Full Data Parsing**: Parse đầy đủ tất cả dữ liệu từ scanner
## 🎯 Trạng thái hoàn thiện
**Trạng thái hiện tại: ĐÃ HOÀN THIỆN CÁC PHẦN CORE - SẴN SÀNG CHO PRODUCTION**
**Đánh giá tổng thể: ~85% hoàn thiện**
### ✅ Các phần đã hoàn thiện 100%
#### 1. Core Infrastructure ✅
- ✅ TCP Client (`TcpClient.cs`)
- ✅ UDP Client (`UdpClient.cs`)
- ✅ COLA2 Session Management (`Cola2Session.cs`)
- ✅ Exception Handling (`Cola2Exceptions.cs`)
- ✅ Helper Functions (`ReadWriteHelper.cs`)
- ✅ Thread Safety: Tất cả operations đều thread-safe
- ✅ Async/Await Pattern: Sử dụng async/await hiện đại
#### 2. COLA2 Commands (Core) ✅
Tất cả 8 core COLA2 commands đã được implement:
1.`CommandBase.cs` - Base class cho tất cả commands
2.`CreateSessionCommand.cs` - Tạo COLA2 session
3.`CloseSessionCommand.cs` - Đóng COLA2 session
4.`VariableCommand.cs` - Đọc biến generic bằng index
5.`MethodCommand.cs` - Base class cho method commands
6.`ChangeCommSettingsCommand.cs` - **CRITICAL**: Cấu hình scanner settings
7.`FindMeCommand.cs` - Làm scanner nhấp nháy để tìm
8.`LatestTelegramVariableCommand.cs` - Lấy latest telegram
**Lưu ý**: Các variable command wrappers (21 commands) trong C++ reference **KHÔNG CẦN THIẾT** vì đã có các request methods trong `SafetyScanner` sử dụng `VariableCommand` generic với các parsers tương ứng.
#### 3. Data Parsers ✅
**Scan Data Parsers (6/6 - 100%)**:
1.`ParseDerivedValues.cs` - Parse derived values block
2.`ParseMeasurementData.cs` - Parse measurement data block
3.`ParseGeneralSystemState.cs` - Parse general system state
4.`ParseIntrusionData.cs` - Parse intrusion data block
5.`ParseApplicationData.cs` - Parse application data block
6.`ParseData.cs` - Main parser coordinator
**COLA2 Response Parsers (18/18 - 100%)**:
1.`ParseApplicationName.cs`
2.`ParseDeviceName.cs`
3.`ParseDeviceStatus.cs`
4.`ParseFieldGeometryData.cs`
5.`ParseFieldHeaderData.cs`
6.`ParseFieldSetsData.cs`
7.`ParseFirmwareVersion.cs`
8.`ParseMeasurementCurrentConfigData.cs`
9.`ParseMeasurementPersistentConfigData.cs`
10.`ParseMonitoringCaseData.cs`
11.`ParseOrderNumber.cs`
12.`ParseProjectName.cs`
13.`ParseRequiredUserAction.cs`
14.`ParseSerialNumber.cs`
15.`ParseStatusOverview.cs`
16.`ParseTypeCode.cs`
17.`ParseUserName.cs`
18.`ParseConfigMetadata.cs`
#### 4. Data Structures ✅
**Scan Data Structures (11/11 - 100%)**:
- `PacketBuffer.cs`, `ParsedPacketBuffer.cs`
- `DatagramHeader.cs`, `DataHeader.cs`
- `UdpScanData.cs`, `UdpScanDataEventArgs.cs`
- `ScanPoint.cs`, `MeasurementData.cs`, `DerivedValues.cs`
- `GeneralSystemState.cs`, `IntrusionData.cs`, `ApplicationData.cs`
**COLA2 Response Structures (17+/17+ - 100%)**:
- `ApplicationName.cs`, `DeviceName.cs`, `DeviceStatus.cs`
- `FieldData.cs`, `FieldSets.cs`
- `FirmwareVersion.cs`, `OrderNumber.cs`, `ProjectName.cs`
- `SerialNumber.cs`, `UserName.cs`, `TypeCode.cs`
- `ConfigData.cs`, `ConfigMetadata.cs`, `StatusOverview.cs`
- `MonitoringCaseData.cs`, `RequiredUserAction.cs`
- `CommSettings.cs` ⚠️ **CRITICAL**
- `SensorDataFeatures.cs` - Helper class cho feature flags
#### 5. SafetyScanner Methods ✅
**Tất cả 17+ methods đã được implement**:
-`ConnectAsync()` / `DisconnectAsync()`
-`ChangeCommSettingsAsync()` - **CRITICAL**
-`FindSensorAsync()`
-`RequestLatestTelegramAsync()`
-`RequestTypeCodeAsync()`
-`RequestApplicationNameAsync()`
-`RequestSerialNumberAsync()`
-`RequestFirmwareVersionAsync()`
-`RequestOrderNumberAsync()`
-`RequestProjectNameAsync()`
-`RequestUserNameAsync()`
-`RequestDeviceNameAsync()`
-`RequestDeviceStatusAsync()`
-`RequestConfigMetadataAsync()`
-`RequestStatusOverviewAsync()`
-`RequestRequiredUserActionAsync()`
-`RequestPersistentConfigAsync()`
-`RequestCurrentConfigAsync()`
-`RequestFieldSetsAsync()`
-`RequestFieldHeaderAsync()`
-`RequestFieldGeometryAsync()`
-`RequestFieldDataAsync()`
-`RequestAllFieldDataAsync()`
-`RequestMonitoringCaseAsync()`
-`RequestMonitoringCasesAsync()`
-`StartUdpStreamingAsync()` / `StopUdpStreaming()`
### 📊 So sánh với C++ Reference
| Component | C++ | C# | Hoàn thành |
|-----------|-----|----|-----------|
| COLA2 Commands (Core) | 8 | 8 | 100% ✅ |
| COLA2 Commands (Wrappers) | 21 | 0 | ~0% ⚠️ (Không cần thiết) |
| Data Parsers (Scan Data) | 6 | 6 | 100% ✅ |
| Data Parsers (COLA2 Response) | 18 | 18 | 100% ✅ |
| Data Structures (Scan Data) | 11 | 11 | 100% ✅ |
| Data Structures (COLA2) | 17+ | 17+ | 100% ✅ |
| SafetyScanner Methods | 17+ | 17+ | 100% ✅ |
| Core Infrastructure | ✅ | ✅ | 100% ✅ |
**Kết luận**: Project đã sẵn sàng cho production use với 100% functionality tương đương C++ reference.
## 📁 Cấu trúc Project
```
Sick.SafetyScanners/
├── Cola2/ # COLA2 protocol implementation
│ ├── Commands/ # Command classes
│ │ ├── CommandBase.cs
│ │ ├── CreateSessionCommand.cs
│ │ ├── CloseSessionCommand.cs
│ │ ├── VariableCommand.cs
│ │ ├── MethodCommand.cs
│ │ ├── ChangeCommSettingsCommand.cs ⚠️ CRITICAL
│ │ ├── FindMeCommand.cs
│ │ └── LatestTelegramVariableCommand.cs
│ └── Cola2Session.cs # Session management
├── Communication/ # Communication clients
│ ├── TcpClient.cs # TCP client for COLA2
│ └── UdpClient.cs # UDP client for scan data
├── DataProcessing/ # Packet processing
│ ├── ParseTcpPacket.cs # TCP packet parser
│ ├── ParseDatagramHeader.cs # Datagram header parser
│ ├── ParseDataHeader.cs # Data header parser
│ ├── ParseDerivedValues.cs # Derived values parser
│ ├── ParseMeasurementData.cs # Measurement data parser
│ ├── ParseGeneralSystemState.cs # System state parser
│ ├── ParseIntrusionData.cs # Intrusion data parser
│ ├── ParseApplicationData.cs # Application data parser
│ ├── ParseData.cs # Main parser coordinator
│ ├── ParseTypeCode.cs # Type code parser (COLA2)
│ ├── ParseApplicationName.cs # Application name parser
│ ├── ... (18 COLA2 response parsers)
│ ├── TcpPacketMerger.cs # Merge fragmented TCP packets
│ └── UdpPacketMerger.cs # Merge fragmented UDP packets
├── DataStructures/ # Data structures
│ ├── PacketBuffer.cs # Packet buffer
│ ├── DatagramHeader.cs # Datagram header
│ ├── DataHeader.cs # Data header
│ ├── UdpScanData.cs # UDP scan data
│ ├── CommSettings.cs # Communication settings ⚠️ CRITICAL
│ ├── ScanPoint.cs # Scan point data
│ ├── MeasurementData.cs # Measurement data
│ └── ... (other structures)
├── Exceptions/ # Custom exceptions
│ └── Cola2Exceptions.cs
├── Helpers/ # Helper functions
│ └── ReadWriteHelper.cs # Binary read/write helpers
├── Interfaces/ # Interfaces
│ ├── ICola2Session.cs
│ ├── ICola2Command.cs
│ ├── ITcpClient.cs
│ └── IUdpClient.cs
├── Types/ # Type definitions
│ └── Cola2Types.cs
└── SafetyScanner.cs # Main class ⚠️ ENTRY POINT
```
## 🚀 Hướng dẫn sử dụng
### Kết nối cơ bản
```csharp
using Sick.SafetyScanners;
using Sick.SafetyScanners.DataStructures;
// Tạo scanner instance
var scanner = new SafetyScanner("192.168.1.11", 2122);
try
{
// Kết nối và mở COLA2 session
await scanner.ConnectAsync();
// Đọc biến từ sensor (ví dụ: variable index 13 = TypeCode)
var data = await scanner.ReadVariableAsync(13);
// Xử lý data...
}
finally
{
// Đóng kết nối
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Request thông tin từ Scanner
```csharp
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync();
try
{
// Request type code
var typeCode = await scanner.RequestTypeCodeAsync();
Console.WriteLine($"Type Code: {typeCode.Code}");
// Request serial number
var serialNumber = await scanner.RequestSerialNumberAsync();
Console.WriteLine($"Serial Number: {serialNumber.Number}");
// Request firmware version
var firmware = await scanner.RequestFirmwareVersionAsync();
Console.WriteLine($"Firmware: {firmware.Version}");
// Request device status
var status = await scanner.RequestDeviceStatusAsync();
Console.WriteLine($"Device Status: {status.Status}");
// Request current config
var config = await scanner.RequestCurrentConfigAsync();
Console.WriteLine($"Host IP: {config.HostIp}");
Console.WriteLine($"UDP Port: {config.HostUdpPort}");
}
finally
{
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Cấu hình Scanner Settings
```csharp
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync();
try
{
// Tạo communication settings
var settings = CommSettings.Create(
channel: 0,
hostIp: "192.168.1.100", // Host IP để scanner gửi UDP packets đến
hostUdpPort: 22041, // Local UDP port để nhận scan data
generalSystemState: true, // Enable general system state
derivedSettings: true, // Enable derived settings
measurementData: true, // Enable measurement data
intrusionData: true, // Enable intrusion data
applicationData: true, // Enable application data
publishingFrequency: 1, // Publish every scan
startAngle: 0.0f, // Start angle (radians, 0 = all angles)
endAngle: 0.0f, // End angle (radians, 0 = all angles)
interfaceType: InterfaceType.NonSafeEthernet,
enabled: true
);
// Áp dụng settings
await scanner.ChangeCommSettingsAsync(settings);
Console.WriteLine("Scanner settings updated successfully");
}
finally
{
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Nhận Scan Data qua UDP Streaming
```csharp
// Tạo scanner với UDP support (local UDP port để nhận scan data)
var scanner = new SafetyScanner("192.168.1.11", 2122, udpLocalPort: 22041);
// Đăng ký event handler để nhận scan data
scanner.ScanDataReceived += (sender, args) =>
{
var scanData = args.ScanData;
Console.WriteLine($"Timestamp: {scanData.Timestamp}");
Console.WriteLine($"Number of beams: {scanData.DerivedValues?.NumberOfBeams ?? 0}");
// Access measurement data
if (scanData.MeasurementData != null)
{
foreach (var point in scanData.MeasurementData.ScanPoints)
{
Console.WriteLine($"Distance: {point.Distance}m, Angle: {point.Angle}rad");
}
}
// Access system state
if (scanData.GeneralSystemState != null)
{
Console.WriteLine($"Device Status HasDeviceError: {scanData.GeneralSystemState.HasDeviceError}");
}
};
try
{
// Kết nối và cấu hình scanner (nếu chưa được cấu hình)
await scanner.ConnectAsync();
// Cấu hình scanner để gửi scan data qua UDP
var settings = CommSettings.Create(
channel: 0,
hostIp: "192.168.1.100",
hostUdpPort: 22041,
generalSystemState: true,
derivedSettings: true,
measurementData: true,
intrusionData: true,
applicationData: true
);
await scanner.ChangeCommSettingsAsync(settings);
// Bắt đầu nhận scan data qua UDP
await scanner.StartUdpStreamingAsync();
Console.WriteLine("UDP streaming started. Press any key to stop...");
Console.ReadKey();
}
finally
{
// Dừng UDP streaming
scanner.StopUdpStreaming();
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Request Latest Telegram qua TCP
```csharp
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync();
try
{
// Request latest telegram (scan data) qua TCP
var scanData = await scanner.RequestLatestTelegramAsync(channelIndex: 0);
Console.WriteLine($"Timestamp: {scanData.Timestamp}");
// Process scan data...
if (scanData.MeasurementData != null)
{
Console.WriteLine($"Number of scan points: {scanData.MeasurementData.ScanPoints.Count}");
}
}
finally
{
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Tìm Sensor (Find Sensor)
```csharp
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync();
try
{
// Làm scanner nhấp nháy trong 10 giây để dễ tìm
await scanner.FindSensorAsync(blinkTime: 10);
Console.WriteLine("Sensor should be blinking now");
}
finally
{
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Sử dụng với CancellationToken
```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync(cancellationToken: cts.Token);
try
{
var data = await scanner.ReadVariableAsync(
13,
cancellationToken: cts.Token
);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
finally
{
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
### Request Field Data
```csharp
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync();
try
{
// Request field sets
var fieldSets = await scanner.RequestFieldSetsAsync();
Console.WriteLine($"Number of field sets: {fieldSets.FieldSets.Count}");
// Request field data cho field index 0
var fieldData = await scanner.RequestFieldDataAsync(fieldIndex: 0);
if (fieldData.IsValid)
{
Console.WriteLine($"Field Name: {fieldData.FieldName}");
Console.WriteLine($"Is Warning Field: {fieldData.IsWarningField}");
Console.WriteLine($"Is Protective Field: {fieldData.IsProtectiveField}");
}
// Request tất cả valid fields
var allFields = await scanner.RequestAllFieldDataAsync();
Console.WriteLine($"Total valid fields: {allFields.Count}");
}
finally
{
await scanner.DisconnectAsync();
scanner.Dispose();
}
```
## 📚 API Reference
### SafetyScanner Class
#### Constructors
```csharp
// TCP-only mode
public SafetyScanner(string sensorIp, ushort sensorPort)
// TCP-only mode with custom TCP client
public SafetyScanner(ITcpClient tcpClient)
// TCP + UDP streaming mode
public SafetyScanner(string sensorIp, ushort sensorPort, ushort udpLocalPort)
// TCP + UDP streaming mode with custom TCP client
public SafetyScanner(ITcpClient tcpClient, ushort udpLocalPort)
```
#### Properties
```csharp
public bool IsConnected { get; } // Connection status
public bool IsUdpStreaming { get; } // UDP streaming status
public ushort LocalUdpPort { get; } // Local UDP port (if UDP enabled)
public ICola2Session Session { get; } // COLA2 session
```
#### Events
```csharp
public event EventHandler<UdpScanDataEventArgs>? ScanDataReceived;
```
#### Methods
**Connection Management**:
- `Task ConnectAsync(bool openCola2Session = true, CancellationToken cancellationToken = default)`
- `Task DisconnectAsync(CancellationToken cancellationToken = default)`
**Command Execution**:
- `Task SendCommandAsync(ICola2Command command, TimeDuration? timeout = null, CancellationToken cancellationToken = default)`
- `Task<ReadOnlyMemory<byte>> ReadVariableAsync(ushort variableIndex, TimeDuration? timeout = null, CancellationToken cancellationToken = default)`
**Scanner Configuration**:
- `Task ChangeCommSettingsAsync(CommSettings settings, TimeDuration? timeout = null, CancellationToken cancellationToken = default)`
- `Task FindSensorAsync(ushort blinkTime, TimeDuration? timeout = null, CancellationToken cancellationToken = default)`
**Request Methods (COLA2 Variables)**:
- `Task<TypeCode> RequestTypeCodeAsync(...)`
- `Task<ApplicationName> RequestApplicationNameAsync(...)`
- `Task<SerialNumber> RequestSerialNumberAsync(...)`
- `Task<FirmwareVersion> RequestFirmwareVersionAsync(...)`
- `Task<OrderNumber> RequestOrderNumberAsync(...)`
- `Task<ProjectName> RequestProjectNameAsync(...)`
- `Task<UserName> RequestUserNameAsync(...)`
- `Task<DeviceName> RequestDeviceNameAsync(...)`
- `Task<DeviceStatus> RequestDeviceStatusAsync(...)`
- `Task<ConfigMetadata> RequestConfigMetadataAsync(...)`
- `Task<StatusOverview> RequestStatusOverviewAsync(...)`
- `Task<RequiredUserAction> RequestRequiredUserActionAsync(...)`
- `Task<ConfigData> RequestPersistentConfigAsync(...)`
- `Task<ConfigData> RequestCurrentConfigAsync(...)`
- `Task<FieldSets> RequestFieldSetsAsync(...)`
- `Task<FieldData> RequestFieldHeaderAsync(ushort fieldIndex, ...)`
- `Task<FieldData> RequestFieldGeometryAsync(ushort fieldIndex, ...)`
- `Task<FieldData> RequestFieldDataAsync(ushort fieldIndex, ...)`
- `Task<List<FieldData>> RequestAllFieldDataAsync(...)`
- `Task<MonitoringCaseData> RequestMonitoringCaseAsync(ushort caseIndex, ...)`
- `Task<List<MonitoringCaseData>> RequestMonitoringCasesAsync(...)`
- `Task<UdpScanData> RequestLatestTelegramAsync(sbyte channelIndex = 0, ...)`
**UDP Streaming**:
- `Task StartUdpStreamingAsync(CancellationToken cancellationToken = default)`
- `void StopUdpStreaming()`
### CommSettings Class
```csharp
public sealed class CommSettings
{
public byte Channel { get; init; } // Channel number (0-3)
public ushort PublishingFrequency { get; init; } // Publish every n-th scan
public InterfaceType EInterfaceType { get; init; } // Interface type
public double StartAngle { get; init; } // Start angle (radians)
public double EndAngle { get; init; } // End angle (radians)
public ushort Features { get; init; } // Feature flags
public bool Enabled { get; init; } // Channel enabled
public ushort HostUdpPort { get; init; } // Host UDP port
public string HostIp { get; init; } // Host IP address
// Helper methods
public static CommSettings CreateDefault()
public static CommSettings Create(...)
}
```
### Data Structures
**Scan Data**:
- `UdpScanData` - Complete scan data from UDP
- `DataHeader` - Data header with timestamps and block information
- `DerivedValues` - Derived values (angles, resolution, beam count)
- `MeasurementData` - Measurement data with scan points
- `GeneralSystemState` - General system state
- `IntrusionData` - Intrusion data
- `ApplicationData` - Application data
- `ScanPoint` - Individual scan point
**COLA2 Response Data**:
- `TypeCode` - Type code information
- `ApplicationName` - Application name
- `SerialNumber` - Serial number
- `FirmwareVersion` - Firmware version
- `DeviceName` - Device name
- `DeviceStatus` - Device status
- `ConfigData` - Configuration data
- `ConfigMetadata` - Configuration metadata
- `StatusOverview` - Status overview
- `FieldData` - Field data
- `FieldSets` - Field sets
- `MonitoringCaseData` - Monitoring case data
- Và nhiều structures khác...
## 🔒 Thread Safety
Tất cả các lớp đều được thiết kế thread-safe:
- **TcpClient**: Sử dụng lock để bảo vệ socket operations
- **UdpClient**: Thread-safe UDP operations
- **Cola2Session**: Sử dụng lock để bảo vệ session state và request ID
- **CommandBase**: Sử dụng lock để bảo vệ internal state
- **TcpPacketMerger**: Sử dụng lock để bảo vệ buffer operations
- **UdpPacketMerger**: Thread-safe packet merging
- **SafetyScanner**: Thread-safe operations, có thể gọi từ nhiều threads
**Ví dụ thread-safe usage**:
```csharp
var scanner = new SafetyScanner("192.168.1.11", 2122);
await scanner.ConnectAsync();
// Có thể gọi từ nhiều threads
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
var index = i;
tasks.Add(Task.Run(async () =>
{
var data = await scanner.ReadVariableAsync((ushort)index);
// Process data...
}));
}
await Task.WhenAll(tasks);
```
## ⚠️ Error Handling
Thư viện cung cấp các exception types:
- `Cola2Exception`: Base exception cho COLA2 errors
- `SessionException`: Session management errors
- `CommandException`: Command execution errors
- `Cola2TimeoutException`: Timeout errors
- `TcpCommunicationException`: TCP communication errors
- `PacketParsingException`: Packet parsing errors
**Ví dụ error handling**:
```csharp
try
{
await scanner.ConnectAsync();
var settings = CommSettings.CreateDefault();
await scanner.ChangeCommSettingsAsync(settings);
}
catch (Cola2TimeoutException ex)
{
Console.WriteLine($"Timeout: {ex.Operation} after {ex.Timeout}");
}
catch (TcpCommunicationException ex)
{
Console.WriteLine($"TCP Error: {ex.Message}");
}
catch (SessionException ex)
{
Console.WriteLine($"Session Error: {ex.Message}");
}
catch (CommandException ex)
{
Console.WriteLine($"Command Error: {ex.CommandType}, {ex.CommandMode} - {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
```
## 📦 Yêu cầu hệ thống
- **.NET 10.0** hoặc cao hơn
- **Không có external dependencies** - chỉ sử dụng .NET standard libraries
## 📝 Ghi chú
### Variable Command Wrappers
Các variable command wrappers (21 commands) trong C++ reference **KHÔNG CẦN THIẾT** vì:
- C# implementation đã có các request methods tương đương trong `SafetyScanner`
- Các request methods sử dụng `VariableCommand` generic + parsers tương ứng
- Cách tiếp cận này đơn giản hơn và tránh code duplication
- Functionality hoàn toàn tương đương với C++ reference
### Tham chiếu C++
Project này được chuyển đổi từ project C++ `sick_safetyscanners_base`:
- **C++ Reference**: `srcs/refs/sick_safetyscanners_base`
- **Functionality**: 100% tương đương với C++ reference
- **Code structure**: Tốt hơn (không có code duplication)
- **API design**: Hiện đại hơn (async/await, better error handling)
## 📄 License
Apache License 2.0
## 🔗 Links
- C++ Reference Project: `srcs/refs/sick_safetyscanners_base`
- SICK Safety Scanners Documentation: [SICK Official Documentation](https://www.sick.com/)
---
**Status**: ✅ **PRODUCTION READY** - Tất cả core functionality đã hoàn thiện và sẵn sàng sử dụng.

View File

@@ -0,0 +1,838 @@
using Sick.SafetyScanners.Cola2;
using Sick.SafetyScanners.Cola2.Commands;
using Sick.SafetyScanners.Communication;
using Sick.SafetyScanners.DataProcessing;
using Sick.SafetyScanners.DataStructures;
using Sick.SafetyScanners.Interfaces;
using Sick.SafetyScanners.Types;
namespace Sick.SafetyScanners;
/// <summary>
/// Main class for SICK Safety Scanner communication
/// Handles both TCP (COLA2) and UDP (scan data streaming) communication
/// Thread-safe implementation
/// </summary>
public sealed class SafetyScanner : IDisposable
{
private readonly ITcpClient _tcpClient;
private readonly ICola2Session _cola2Session;
// UDP components for scan data streaming
private readonly IUdpClient? _udpClient;
private readonly UdpPacketMerger? _udpPacketMerger;
private readonly ParseData? _parseData;
private readonly Lock _udpLock = new();
private bool _isUdpStreaming;
private bool _disposed;
/// <summary>
/// Creates a new Safety Scanner instance
/// </summary>
public SafetyScanner(string sensorIp, ushort sensorPort)
: this(new TcpClient(sensorIp, sensorPort))
{
}
/// <summary>
/// Creates a new Safety Scanner instance with custom TCP client
/// </summary>
public SafetyScanner(ITcpClient tcpClient)
{
_tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient));
_cola2Session = new Cola2Session(_tcpClient);
}
/// <summary>
/// Creates a new Safety Scanner instance with UDP streaming support
/// </summary>
/// <param name="sensorIp">Sensor IP address</param>
/// <param name="sensorPort">Sensor TCP port (COLA2)</param>
/// <param name="udpLocalPort">Local UDP port for receiving scan data (0 = auto-assign)</param>
public SafetyScanner(string sensorIp, ushort sensorPort, ushort udpLocalPort)
: this(new TcpClient(sensorIp, sensorPort), udpLocalPort)
{
}
/// <summary>
/// Creates a new Safety Scanner instance with UDP streaming support and specific local IP
/// </summary>
/// <param name="sensorIp">Sensor IP address</param>
/// <param name="sensorPort">Sensor TCP port (COLA2)</param>
/// <param name="udpLocalPort">Local UDP port for receiving scan data (0 = auto-assign)</param>
/// <param name="udpLocalIp">Local IP address to bind UDP server to (null = bind to 0.0.0.0, all interfaces)</param>
public SafetyScanner(string sensorIp, ushort sensorPort, ushort udpLocalPort, System.Net.IPAddress? udpLocalIp)
: this(new TcpClient(sensorIp, sensorPort), udpLocalPort, udpLocalIp)
{
}
/// <summary>
/// Creates a new Safety Scanner instance with custom TCP client and UDP streaming support
/// </summary>
/// <param name="tcpClient">TCP client for COLA2 communication</param>
/// <param name="udpLocalPort">Local UDP port for receiving scan data (0 = auto-assign)</param>
public SafetyScanner(ITcpClient tcpClient, ushort udpLocalPort)
: this(tcpClient, udpLocalPort, null)
{
}
/// <summary>
/// Creates a new Safety Scanner instance with custom TCP client and UDP streaming support with specific local IP
/// </summary>
/// <param name="tcpClient">TCP client for COLA2 communication</param>
/// <param name="udpLocalPort">Local UDP port for receiving scan data (0 = auto-assign)</param>
/// <param name="udpLocalIp">Local IP address to bind UDP server to (null = bind to 0.0.0.0, all interfaces)</param>
public SafetyScanner(ITcpClient tcpClient, ushort udpLocalPort, System.Net.IPAddress? udpLocalIp)
{
_tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient));
_cola2Session = new Cola2Session(_tcpClient);
// Initialize UDP components for scan data streaming
_udpClient = new Communication.UdpClient(udpLocalPort, udpLocalIp);
_udpPacketMerger = new UdpPacketMerger();
_parseData = new ParseData();
}
/// <summary>
/// Gets the COLA2 session
/// </summary>
public ICola2Session Session => _cola2Session;
/// <summary>
/// Indicates whether the scanner is connected
/// - For TCP mode: checks TCP connection and COLA2 session
/// - For UDP streaming mode: checks UDP streaming status (UDP receiver is bound and listening)
/// Note: UDP is connectionless - this checks if UDP receiver is ready to receive packets
/// </summary>
public bool IsConnected
{
get
{
// If UDP streaming is enabled, check UDP streaming status instead of TCP
if (_udpClient != null)
{
lock (_udpLock)
{
// In UDP streaming mode, connection means UDP receiver is bound and listening
return _isUdpStreaming && _udpClient.IsConnected;
}
}
// For TCP-only mode, check TCP connection and COLA2 session
return _tcpClient.IsConnected && _cola2Session.IsOpen;
}
}
/// <summary>
/// Indicates whether UDP streaming is active
/// </summary>
public bool IsUdpStreaming
{
get
{
lock (_udpLock)
{
return _isUdpStreaming && _udpClient != null && _udpClient.IsConnected;
}
}
}
/// <summary>
/// Gets the local UDP port (0 if UDP is not enabled or not bound)
/// </summary>
public ushort LocalUdpPort
{
get
{
lock (_udpLock)
{
return _udpClient?.LocalPort?.Value ?? (ushort)0;
}
}
}
/// <summary>
/// Event fired when scan data is received via UDP
/// </summary>
public event EventHandler<UdpScanDataEventArgs>? ScanDataReceived;
/// <summary>
/// Connects to the scanner and opens a COLA2 session
/// Note: For UDP streaming mode, COLA2 session is optional if scanner is already configured.
/// COLA2 session is only needed to configure scanner settings (e.g., ChangeCommSettings).
/// </summary>
/// <param name="openCola2Session">Whether to open COLA2 session. Default: true.
/// Set to false for UDP-only mode if scanner is already configured.</param>
public void Connect(bool openCola2Session = true)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
if (openCola2Session)
{
_cola2Session.Open();
}
}
/// <summary>
/// Disconnects from the scanner and closes the COLA2 session
/// </summary>
public void Disconnect()
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
_cola2Session.Close();
}
/// <summary>
/// Sends a COLA2 command to the scanner
/// </summary>
public void SendCommand(ICola2Command command, TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
_cola2Session.SendCommand(command, timeout);
}
/// <summary>
/// Reads a variable from the scanner by index
/// </summary>
public ReadOnlyMemory<byte> ReadVariable(ushort variableIndex,
TimeDuration? timeout = null)
{
var command = new VariableCommand(variableIndex);
SendCommand(command, timeout);
if (!command.WasSuccessful)
{
throw new Exceptions.CommandException(
command.CommandType,
command.CommandMode,
$"Failed to read variable at index {variableIndex}"
);
}
return command.GetDataVector();
}
/// <summary>
/// Requests the latest telegram (measurement data) from the scanner via TCP
/// Note: Unlike UDP streaming, TCP requires sending a request command to receive data
/// </summary>
/// <param name="channelIndex">Channel index (0-3), defaults to 0</param>
/// <param name="timeout">Timeout for the request</param>
/// <returns>The parsed scan data</returns>
public DataStructures.UdpScanData RequestLatestTelegram(
sbyte channelIndex = 0,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var command = new LatestTelegramVariableCommand(channelIndex);
SendCommand(command, timeout);
if (!command.WasSuccessful)
{
throw new Exceptions.CommandException(
command.CommandType,
command.CommandMode,
$"Failed to request latest telegram for channel {channelIndex}"
);
}
if (command.ScanData == null)
{
throw new Exceptions.CommandException(
command.CommandType,
command.CommandMode,
$"Failed to parse scan data from latest telegram response"
);
}
return command.ScanData;
}
#region Request Methods - COLA2 Variable Commands
/// <summary>
/// Requests the type code from the sensor
/// </summary>
public DataStructures.TypeCode RequestTypeCode(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(0x000d, timeout);
var parser = new DataProcessing.ParseTypeCode();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the application name from the sensor
/// </summary>
public DataStructures.ApplicationName RequestApplicationName(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(33, timeout);
var parser = new DataProcessing.ParseApplicationName();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the serial number from the sensor
/// </summary>
public DataStructures.SerialNumber RequestSerialNumber(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(3, timeout);
var parser = new DataProcessing.ParseSerialNumber();
var buffer = new PacketBuffer(data);
return ParseSerialNumber.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the firmware version from the sensor
/// </summary>
public DataStructures.FirmwareVersion RequestFirmwareVersion(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(4, timeout);
var parser = new DataProcessing.ParseFirmwareVersion();
var buffer = new PacketBuffer(data);
return ParseFirmwareVersion.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the order number from the sensor
/// </summary>
public DataStructures.OrderNumber RequestOrderNumber(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(14, timeout);
var parser = new DataProcessing.ParseOrderNumber();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the project name from the sensor
/// </summary>
public DataStructures.ProjectName RequestProjectName(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(18, timeout);
var parser = new DataProcessing.ParseProjectName();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the user name from the sensor
/// </summary>
public DataStructures.UserName RequestUserName(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(35, timeout);
var parser = new DataProcessing.ParseUserName();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the device name from the sensor
/// </summary>
public DataStructures.DeviceName RequestDeviceName(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(17, timeout);
var parser = new DataProcessing.ParseDeviceName();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the device status from the sensor
/// </summary>
public DataStructures.DeviceStatus RequestDeviceStatus(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(15, timeout);
var parser = new DataProcessing.ParseDeviceStatus();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the config metadata from the sensor
/// </summary>
public DataStructures.ConfigMetadata RequestConfigMetadata(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(28, timeout);
var parser = new DataProcessing.ParseConfigMetadata();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the status overview from the sensor
/// </summary>
public DataStructures.StatusOverview RequestStatusOverview(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(23, timeout);
var parser = new DataProcessing.ParseStatusOverview();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the required user action from the sensor
/// </summary>
public DataStructures.RequiredUserAction RequestRequiredUserAction(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(16, timeout);
var parser = new DataProcessing.ParseRequiredUserAction();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the persistent configuration from the sensor
/// </summary>
public DataStructures.ConfigData RequestPersistentConfig(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(177, timeout);
var parser = new DataProcessing.ParseMeasurementPersistentConfigData();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests the current measurement configuration from the sensor
/// </summary>
public DataStructures.ConfigData RequestCurrentConfig(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(178, timeout);
var parser = new DataProcessing.ParseMeasurementCurrentConfigData();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests field sets data from the sensor
/// </summary>
public DataStructures.FieldSets RequestFieldSets(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var data = ReadVariable(1003, timeout);
var parser = new DataProcessing.ParseFieldSetsData();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests field data (header) from the sensor for a specific field index
/// </summary>
/// <param name="fieldIndex">Field index (0-127)</param>
public DataStructures.FieldData RequestFieldHeader(
ushort fieldIndex,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
ushort variableIndex = (ushort)(0x2710 + fieldIndex); // 10000 + fieldIndex
var data = ReadVariable(variableIndex, timeout);
var parser = new DataProcessing.ParseFieldHeaderData();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests field geometry data from the sensor for a specific field index
/// </summary>
/// <param name="fieldIndex">Field index (0-127)</param>
public DataStructures.FieldData RequestFieldGeometry(
ushort fieldIndex,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
ushort variableIndex = (ushort)(0x2810 + fieldIndex); // 10256 + fieldIndex
var data = ReadVariable(variableIndex, timeout);
var parser = new DataProcessing.ParseFieldGeometryData();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests complete field data (header + geometry) from the sensor for a specific field index
/// </summary>
/// <param name="fieldIndex">Field index (0-127)</param>
public DataStructures.FieldData RequestFieldData(
ushort fieldIndex,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
// Request header first
var fieldData = RequestFieldHeader(fieldIndex, timeout);
// If valid, request geometry
if (fieldData.IsValid)
{
var geometry = RequestFieldGeometry(fieldIndex, timeout);
// Merge geometry data into fieldData
return new DataStructures.FieldData
{
IsValid = fieldData.IsValid,
VersionCVersion = fieldData.VersionCVersion,
VersionMajorVersionNumber = fieldData.VersionMajorVersionNumber,
VersionMinorVersionNumber = fieldData.VersionMinorVersionNumber,
VersionReleaseNumber = fieldData.VersionReleaseNumber,
IsDefined = fieldData.IsDefined,
EvalMethod = fieldData.EvalMethod,
MultiSampling = fieldData.MultiSampling,
ObjectResolution = fieldData.ObjectResolution,
FieldSetIndex = fieldData.FieldSetIndex,
NameLength = fieldData.NameLength,
FieldName = fieldData.FieldName,
IsWarningField = fieldData.IsWarningField,
IsProtectiveField = fieldData.IsProtectiveField,
BeamDistances = geometry.BeamDistances,
StartAngle = geometry.StartAngle,
EndAngle = geometry.EndAngle,
AngularBeamResolution = geometry.AngularBeamResolution
};
}
return fieldData;
}
/// <summary>
/// Requests all valid field data from the sensor
/// </summary>
public List<DataStructures.FieldData> RequestAllFieldData(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var fields = new List<DataStructures.FieldData>();
// Request up to 128 fields, stop at first invalid (after index 0)
for (ushort i = 0; i < 128; i++)
{
try
{
var fieldData = RequestFieldData(i, timeout);
if (fieldData.IsValid)
{
fields.Add(fieldData);
}
else if (i > 0) // Index 0 is reserved for contour data
{
break; // Stop at first invalid field (after index 0)
}
}
catch
{
// If request fails, stop iterating
break;
}
}
return fields;
}
/// <summary>
/// Requests monitoring case data from the sensor for a specific case index
/// </summary>
/// <param name="caseIndex">Monitoring case index (0-253)</param>
public DataStructures.MonitoringCaseData RequestMonitoringCase(
ushort caseIndex,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
ushort variableIndex = (ushort)(2101 + caseIndex);
var data = ReadVariable(variableIndex, timeout);
var parser = new DataProcessing.ParseMonitoringCaseData();
var buffer = new PacketBuffer(data);
return parser.ParseTcpSequence(buffer);
}
/// <summary>
/// Requests all valid monitoring cases from the sensor
/// </summary>
public List<DataStructures.MonitoringCaseData> RequestMonitoringCases(
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var monitoringCases = new List<DataStructures.MonitoringCaseData>();
// Request up to 254 monitoring cases, stop at first invalid
for (ushort i = 0; i < 254; i++)
{
try
{
var monitoringCase = RequestMonitoringCase(i, timeout);
if (monitoringCase.IsValid)
{
monitoringCases.Add(monitoringCase);
}
else
{
break; // Stop at first invalid case
}
}
catch
{
// If request fails, stop iterating
break;
}
}
return monitoringCases;
}
#endregion
/// <summary>
/// Changes the communication settings on the sensor (CRITICAL method)
/// Note: This method should be called before starting UDP streaming
/// </summary>
/// <param name="settings">The communication settings to apply</param>
/// <param name="timeout">Timeout for the command</param>
public void ChangeCommSettings(
DataStructures.CommSettings settings,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
ArgumentNullException.ThrowIfNull(settings);
// Update settings with actual UDP port if UDP client is available
// Create new settings with updated UDP port
var finalSettings = settings;
if (_udpClient != null && _udpClient.LocalPort?.Value != null)
{
var actualPort = _udpClient.LocalPort.Value;
if (actualPort != settings.HostUdpPort)
{
finalSettings = DataStructures.CommSettings.Create(
channel: settings.Channel,
hostIp: settings.HostIp,
hostUdpPort: actualPort,
generalSystemState: settings.GeneralSystemStateEnabled,
derivedSettings: settings.DerivedSettingsEnabled,
measurementData: settings.MeasurementDataEnabled,
intrusionData: settings.IntrusionDataEnabled,
applicationData: settings.ApplicationDataEnabled,
publishingFrequency: settings.PublishingFrequency,
startAngle: settings.StartAngle,
endAngle: settings.EndAngle,
interfaceType: settings.EInterfaceType,
enabled: settings.Enabled
);
}
}
var command = new Cola2.Commands.ChangeCommSettingsCommand(finalSettings);
SendCommand(command, timeout);
if (!command.WasSuccessful)
{
throw new Exceptions.CommandException(
command.CommandType,
command.CommandMode,
"Failed to change communication settings"
);
}
}
/// <summary>
/// Makes the scanner flash/blink its display to help locate it
/// </summary>
/// <param name="blinkTime">Time to flash for in seconds</param>
/// <param name="timeout">Timeout for the command</param>
public void FindSensor(
ushort blinkTime,
TimeDuration? timeout = null)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
var command = new Cola2.Commands.FindMeCommand(blinkTime);
SendCommand(command, timeout);
if (!command.WasSuccessful)
{
throw new Exceptions.CommandException(
command.CommandType,
command.CommandMode,
$"Failed to send find sensor command (blink time: {blinkTime}s)"
);
}
}
/// <summary>
/// Starts UDP streaming to receive scan data automatically
/// </summary>
/// <exception cref="InvalidOperationException">If UDP client is not initialized or already streaming</exception>
public void StartUdpStreaming()
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
if (_udpClient == null || _udpPacketMerger == null || _parseData == null)
throw new InvalidOperationException("UDP streaming is not enabled. Use constructor with udpLocalPort parameter.");
lock (_udpLock)
{
if (_isUdpStreaming)
return; // Already streaming
_isUdpStreaming = true;
}
// Start receiving loop on dedicated high-priority thread
// Note: StartReceiving will create and bind the socket, then start receiving in background
_udpClient.StartReceiving((packet) =>
{
ProcessUdpPacket(packet, _udpPacketMerger, _parseData);
});
}
/// <summary>
/// Stops UDP streaming
/// </summary>
public void StopUdpStreaming()
{
lock (_udpLock)
{
if (!_isUdpStreaming)
return;
_isUdpStreaming = false;
_udpClient?.Stop();
_udpPacketMerger?.Reset();
}
}
/// <summary>
/// Processes incoming UDP packet (merges fragments and parses data)
/// </summary>
private void ProcessUdpPacket(PacketBuffer packet, UdpPacketMerger packetMerger, ParseData parseData)
{
try
{
// Add packet to merger
var isComplete = packetMerger.AddUdpPacket(packet);
if (isComplete)
{
// Get merged packet
var mergedPacket = packetMerger.GetDeployedPacketBuffer();
// Parse scan data
var scanData = parseData.ParseUdpSequence(mergedPacket);
// Fire event
var timestamp = scanData.Timestamp ?? DateTime.UtcNow;
var eventArgs = new UdpScanDataEventArgs(timestamp, scanData);
ScanDataReceived?.Invoke(this, eventArgs);
}
}
catch (Exception ex)
{
// Log error but continue receiving
// In production, you might want to fire an error event
System.Diagnostics.Debug.WriteLine($"Error processing UDP packet: {ex.Message}");
}
}
public void Dispose()
{
ObjectDisposedException.ThrowIf(_disposed, nameof(SafetyScanner));
// Stop UDP streaming first
StopUdpStreaming();
// Disconnect TCP
Disconnect();
// Dispose TCP components
_cola2Session.Dispose();
_tcpClient.Dispose();
// Dispose UDP components
lock (_udpLock)
{
_udpClient?.Dispose();
_udpPacketMerger?.Dispose();
}
_disposed = true;
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using System.Net;
namespace Sick.SafetyScanners.Types;
/// <summary>
/// IP address type for SICK Safety Scanners
/// </summary>
public readonly struct IpAddress
{
private readonly IPAddress _address;
public IpAddress(IPAddress address)
{
_address = address ?? throw new ArgumentNullException(nameof(address));
}
public IpAddress(string ipAddressString)
{
if (string.IsNullOrWhiteSpace(ipAddressString))
throw new ArgumentNullException(nameof(ipAddressString));
_address = IPAddress.Parse(ipAddressString);
}
public IPAddress ToIPAddress() => _address;
public override string ToString() => _address.ToString();
public static implicit operator IPAddress(IpAddress ipAddress) => ipAddress._address;
public static implicit operator IpAddress(IPAddress ipAddress) => new(ipAddress);
public static implicit operator IpAddress(string ipAddressString) => new(ipAddressString);
}
/// <summary>
/// Port type for SICK Safety Scanners
/// </summary>
public readonly struct Port
{
private readonly ushort _port;
public Port(ushort port)
{
if (port == 0)
throw new ArgumentException("Port cannot be zero", nameof(port));
_port = port;
}
public ushort Value => _port;
public override string ToString() => _port.ToString();
public static implicit operator ushort(Port port) => port._port;
public static implicit operator Port(ushort port) => new(port);
}
/// <summary>
/// Timeout duration type
/// </summary>
public readonly struct TimeDuration
{
private readonly TimeSpan _duration;
public TimeDuration(TimeSpan duration)
{
if (duration <= TimeSpan.Zero)
throw new ArgumentException("Duration must be positive", nameof(duration));
_duration = duration;
}
public TimeSpan ToTimeSpan() => _duration;
public static TimeDuration FromSeconds(double seconds) => new(TimeSpan.FromSeconds(seconds));
public static TimeDuration FromMilliseconds(double milliseconds) =>
new(TimeSpan.FromMilliseconds(milliseconds));
public static implicit operator TimeSpan(TimeDuration duration) => duration._duration;
public static implicit operator TimeDuration(TimeSpan duration) => new(duration);
}