using System.Text;
namespace Sick.ColaB.Parsers;
///
/// Parser for SICK scanner LMDscandata telegram
/// Supports both binary (ColaB) and ASCII (ColaA) formats
///
public static class LmdScandataParser
{
// Constants for validation
private const int MinReasonableDistanceCount = 50;
private const int MaxReasonableDistanceCount = 5000;
///
/// Parse LMDscandata from binary ColaB format
/// This is the primary parser for binary protocol data
///
public static ScanDataResult? ParseBinary(ReadOnlySpan commandData)
{
try
{
// Binary LMDscandata format:
// Starts with ASCII header: "sRA LMDscandata " or "sSN LMDscandata "
// Then binary data follows:
// - Various fields (version, device number, etc.)
// - "DIST1" (5 bytes ASCII) followed by:
// - Scale factor (4 bytes double)
// - Scale offset (4 bytes double)
// - Starting angle (4 bytes signed int, units: 1/10000 degree)
// - Angular step (2 bytes unsigned short, units: 1/10000 degree)
// - Number of items (2 bytes, big endian uint16)
// - Distance values (each 2 bytes, big endian uint16, units: mm)
// - "RSSI1" (5 bytes ASCII) followed by:
// - Number of items (2 bytes, big endian uint16)
// - RSSI values (each 1 or 2 bytes depending on resolution)
if (commandData.Length < 50)
{
return null;
}
// Check if it contains "LMDscandata"
var commandStart = Encoding.ASCII.GetString(commandData[..Math.Min(50, commandData.Length)]);
if (!commandStart.Contains("LMDscandata", StringComparison.Ordinal))
{
return null;
}
double startAngleDeg = 0.0;
double angularStepDeg = 0.0;
double scaleFactor = 1.0;
double scaleOffset = 0.0;
// Find "DIST1" in the data
var dist1Pattern = Encoding.ASCII.GetBytes("DIST1");
var dist1Index = FindPattern(commandData, dist1Pattern);
if (dist1Index == -1)
{
return null;
}
// Parse header fields at fixed offsets after DIST1
int headerStart = dist1Index + 5; // Right after "DIST1"
// Scale factor (4 bytes double, big endian) at offset headerStart
if (headerStart + 4 <= commandData.Length)
{
uint scaleFactorInt = ReadUInt32BigEndian(commandData, headerStart);
scaleFactor = BitConverter.ToSingle(BitConverter.GetBytes(scaleFactorInt), 0);
}
// Scale offset (4 bytes double, big endian) at offset headerStart + 4
if (headerStart + 8 <= commandData.Length)
{
uint scaleOffsetInt = ReadUInt32BigEndian(commandData, headerStart + 4);
scaleOffset = BitConverter.ToSingle(BitConverter.GetBytes(scaleOffsetInt), 0);
}
// Starting angle (4 bytes signed int, big endian) at offset headerStart + 8
if (headerStart + 12 <= commandData.Length)
{
int startAngleInt = (int)ReadUInt32BigEndian(commandData, headerStart + 8);
startAngleDeg = startAngleInt / 10000.0;
}
// Angular step width (2 bytes unsigned short, big endian) at offset headerStart + 12
if (headerStart + 14 <= commandData.Length)
{
ushort angularStepInt = ReadUInt16BigEndian(commandData, headerStart + 12);
angularStepDeg = angularStepInt / 10000.0;
}
// Find distance count - try multiple offsets
if (!TryFindDistanceCount(commandData, headerStart, out ushort distCount, out int distCountOffset))
{
return null;
}
// Read distance values (each 2 bytes, big endian, units: mm)
var distValuesOffset = distCountOffset + 2;
if (distValuesOffset + (distCount * 2) > commandData.Length)
{
return null;
}
var ranges = new List();
for (int i = 0; i < distCount; i++)
{
var offset = distValuesOffset + (i * 2);
if (offset + 2 > commandData.Length)
{
break;
}
// Read uint16 big endian
ushort distValue = ReadUInt16BigEndian(commandData, offset);
// Apply scale factor and convert from mm to meters
double distanceM = (distValue * scaleFactor + scaleOffset) / 1000.0;
ranges.Add(distanceM);
}
// Find "RSSI1" for intensities (optional)
var rssi1Pattern = Encoding.ASCII.GetBytes("RSSI1");
var rssi1Index = FindPattern(commandData, rssi1Pattern, rssiSearchStart: distValuesOffset + (distCount * 2));
var intensities = new List();
if (rssi1Index != -1)
{
// Read RSSI count
var rssiCountOffset = rssi1Index + 5;
if (rssiCountOffset + 2 <= commandData.Length)
{
ushort rssiCount = ReadUInt16BigEndian(commandData, rssiCountOffset);
// Read RSSI values (usually 1 byte each, but can be 2 bytes for 16-bit resolution)
var rssiValuesOffset = rssiCountOffset + 2;
// Try to determine if RSSI is 8-bit or 16-bit
bool use16BitRssi = (rssiValuesOffset + (rssiCount * 2) <= commandData.Length) &&
(rssiCount == ranges.Count);
if (use16BitRssi)
{
// 16-bit RSSI values (big endian)
for (int i = 0; i < rssiCount && i < ranges.Count; i++)
{
ushort rssiValue = ReadUInt16BigEndian(commandData, rssiValuesOffset + (i * 2));
// Normalize to 0-1 range (assuming max value is 65535)
intensities.Add(rssiValue);
}
}
else if (rssiValuesOffset + rssiCount <= commandData.Length)
{
// 8-bit RSSI values
for (int i = 0; i < rssiCount && i < ranges.Count; i++)
{
// Normalize to 0-1 range (assuming max value is 255)
intensities.Add(commandData[rssiValuesOffset + i]);
}
}
else
{
for (int i = 0; i < ranges.Count; i++)
{
intensities.Add(1.0);
}
}
}
}
if (ranges.Count == 0)
{
return null;
}
return new ScanDataResult
{
Ranges = [.. ranges],
Intensities = intensities.Count > 0 ? [.. intensities] : null,
StartAngleDeg = startAngleDeg,
AngularStepDeg = angularStepDeg,
ScaleFactor = scaleFactor,
ScaleOffset = scaleOffset,
Timestamp = DateTime.UtcNow
};
}
catch
{
return null;
}
}
///
/// Parse LMDscandata from ASCII format (via ColaB decoding)
/// This is a fallback parser when binary parsing fails
///
public static ScanDataResult? ParseAscii(ReadOnlySpan commandData)
{
try
{
// Decode command string to check if it's scan data
var commandString = ColaBHelper.DecodeCommand(commandData);
// Check if this is scan data telegram
if (!commandString.Contains("LMDscandata", StringComparison.Ordinal))
{
return null;
}
return ParseAsciiString(commandString);
}
catch
{
return null;
}
}
///
/// Parse LMDscandata from ASCII string format
/// Format: "sSN LMDscandata ... DIST1 ... RSSI1 ..."
///
public static ScanDataResult? ParseAsciiString(string telegram)
{
try
{
// Split telegram into fields (space-separated)
var fields = telegram.Split([' '], StringSplitOptions.RemoveEmptyEntries);
if (fields.Length < 20)
{
return null;
}
var ranges = new List();
var intensities = new List();
int distIndex = -1;
int rssiIndex = -1;
// Find DIST1 field
for (int i = 0; i < fields.Length; i++)
{
if (fields[i].StartsWith("DIST", StringComparison.Ordinal))
{
distIndex = i;
break;
}
}
// Find RSSI1 field
for (int i = 0; i < fields.Length; i++)
{
if (fields[i].StartsWith("RSSI", StringComparison.Ordinal))
{
rssiIndex = i;
break;
}
}
// Parse distances
if (distIndex >= 0 && distIndex + 1 < fields.Length)
{
// Next field after DIST1 is the count (in hex)
if (int.TryParse(fields[distIndex + 1], System.Globalization.NumberStyles.HexNumber, null, out int distCount))
{
// Parse distance values (in hex, units are mm, convert to meters)
for (int i = 0; i < distCount && distIndex + 2 + i < fields.Length; i++)
{
if (int.TryParse(fields[distIndex + 2 + i], System.Globalization.NumberStyles.HexNumber, null, out int distValue))
{
// Convert from mm to meters
ranges.Add(distValue / 1000.0);
}
}
}
}
// Parse intensities (RSSI)
if (rssiIndex >= 0 && rssiIndex + 1 < fields.Length)
{
// Next field after RSSI1 is the count (in hex)
if (int.TryParse(fields[rssiIndex + 1], System.Globalization.NumberStyles.HexNumber, null, out int rssiCount))
{
// Parse RSSI values (in hex)
for (int i = 0; i < rssiCount && rssiIndex + 2 + i < fields.Length; i++)
{
if (int.TryParse(fields[rssiIndex + 2 + i], System.Globalization.NumberStyles.HexNumber, null, out int rssiValue))
{
intensities.Add(rssiValue);
}
}
}
}
if (ranges.Count == 0)
{
return null;
}
return new ScanDataResult
{
Ranges = [.. ranges],
Intensities = intensities.Count > 0 ? [.. intensities] : null,
StartAngleDeg = 0.0, // ASCII format doesn't include angle info
AngularStepDeg = 0.0,
ScaleFactor = 1.0,
ScaleOffset = 0.0,
Timestamp = DateTime.UtcNow
};
}
catch
{
return null;
}
}
#region Helper Methods
private static int FindPattern(ReadOnlySpan data, byte[] pattern, int rssiSearchStart = 0)
{
int searchStart = rssiSearchStart;
for (int i = searchStart; i <= data.Length - pattern.Length; i++)
{
bool found = true;
for (int j = 0; j < pattern.Length; j++)
{
if (data[i + j] != pattern[j])
{
found = false;
break;
}
}
if (found)
{
return i;
}
}
return -1;
}
private static uint ReadUInt32BigEndian(ReadOnlySpan data, int offset)
{
if (offset + 4 > data.Length)
return 0;
return (uint)((data[offset] << 24) | (data[offset + 1] << 16) |
(data[offset + 2] << 8) | data[offset + 3]);
}
private static ushort ReadUInt16BigEndian(ReadOnlySpan data, int offset)
{
if (offset + 2 > data.Length)
return 0;
return (ushort)((data[offset] << 8) | data[offset + 1]);
}
private static bool TryFindDistanceCount(ReadOnlySpan commandData, int headerStart,
out ushort distCount, out int distCountOffset)
{
distCount = 0;
distCountOffset = -1;
// Try offset +14 first (matches actual data)
int tryOffset1 = headerStart + 14;
if (TryReadDistanceCount(commandData, tryOffset1, out distCount, out distCountOffset))
return true;
// Try offset +19 (per C++ reference code)
int tryOffset2 = headerStart + 19;
if (TryReadDistanceCount(commandData, tryOffset2, out distCount, out distCountOffset))
return true;
// Search for valid count
int searchStart = headerStart;
int searchEnd = Math.Min(headerStart + 30, commandData.Length - 2);
for (int offset = searchStart; offset <= searchEnd; offset++)
{
if (TryReadDistanceCount(commandData, offset, out distCount, out distCountOffset))
return true;
}
return false;
}
private static bool TryReadDistanceCount(ReadOnlySpan commandData, int offset,
out ushort count, out int countOffset)
{
count = 0;
countOffset = -1;
if (offset + 2 > commandData.Length)
return false;
ushort testCount = ReadUInt16BigEndian(commandData, offset);
if (testCount >= MinReasonableDistanceCount && testCount <= MaxReasonableDistanceCount)
{
int requiredBytes = offset + 2 + (testCount * 2);
if (requiredBytes <= commandData.Length)
{
count = testCount;
countOffset = offset;
return true;
}
}
return false;
}
#endregion
}