using System.Runtime.InteropServices;
using RobotNet10.Shared;
using RobotNet10.Shared.Geometry;
using RobotNet10.Shared.Sensor;
// using RobotNet10.Shared.Numbers;
namespace RobotNet10.RobotApp.Xloc;
///
/// Extension methods to convert C# sensor structs to xloc C-compatible structs
///
public static class XlocConversionExtensions
{
///
/// Convert C# Header to xloc_header_t
/// NOTE: Caller must free FrameId using Marshal.FreeHGlobal
///
public static xloc_header_t ToXlocHeader(this Header header)
{
var xlocHeader = new xloc_header_t
{
seq = header.Seq,
stamp = header.Stamp.ToXlocUnixTime(),
frame_id = IntPtr.Zero
};
// Marshal frame_id string to unmanaged memory
if (!string.IsNullOrEmpty(header.FrameId))
{
xlocHeader.frame_id = Marshal.StringToHGlobalAnsi(header.FrameId);
}
return xlocHeader;
}
///
/// Convert DateTime to xloc_unix_time_t (Unix epoch time)
///
// public static xloc_unix_time_t ToXlocUnixTime(this DateTime timestamp)
// {
// // Convert to Unix time (seconds and nanoseconds since 1970-01-01)
// var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// var duration = timestamp.ToUniversalTime() - epoch;
// var totalSeconds = (long)duration.TotalSeconds;
// var nanoseconds = (long)((duration.TotalSeconds - totalSeconds) * 1_000_000_000);
// var xlocTime = new xloc_unix_time_t
// {
// sec = (uint)totalSeconds,
// nsec = (uint)nanoseconds
// };
// return xlocTime;
// }
public static xloc_unix_time_t ToXlocUnixTime(this DateTime timestamp)
{
var utc = timestamp.Kind == DateTimeKind.Utc
? timestamp
: timestamp.ToUniversalTime();
long ticksSinceEpoch = utc.Ticks - DateTime.UnixEpoch.Ticks;
long totalNanoseconds = ticksSinceEpoch * 100; // 1 tick = 100 ns
uint sec = (uint)(totalNanoseconds / 1_000_000_000);
uint nsec = (uint)(totalNanoseconds % 1_000_000_000);
return new xloc_unix_time_t
{
sec = sec,
nsec = nsec
};
}
///
/// Convert C# Odometry to xloc_odometry_t
/// NOTE: Caller must free frame_id and child_frame_id using FreeXlocOdometry
///
public static xloc_odometry_t ToXlocOdometry(this Odometry odom)
{
var xlocOdom = new xloc_odometry_t
{
header = odom.Header.ToXlocHeader(),
child_frame_id = Marshal.StringToHGlobalAnsi(odom.ChildFrameId ?? string.Empty),
// Pose - flattened arrays (matching C API)
pose_position = new double[]
{
odom.Pose.Pose.Position.X,
odom.Pose.Pose.Position.Y,
odom.Pose.Pose.Position.Z
},
pose_orientation = new double[]
{
odom.Pose.Pose.Orientation.X,
odom.Pose.Pose.Orientation.Y,
odom.Pose.Pose.Orientation.Z,
odom.Pose.Pose.Orientation.W
},
pose_covariance = odom.Pose.Covariance ?? new double[36],
// Twist - flattened arrays (matching C API)
twist_linear = new double[]
{
odom.Twist.Twist.Linear.X,
odom.Twist.Twist.Linear.Y,
odom.Twist.Twist.Linear.Z
},
twist_angular = new double[]
{
odom.Twist.Twist.Angular.X,
odom.Twist.Twist.Angular.Y,
odom.Twist.Twist.Angular.Z
},
twist_covariance = odom.Twist.Covariance ?? new double[36]
};
return xlocOdom;
}
///
/// Convert C# Imu to xloc_imu_t
/// NOTE: Caller must free FrameId using FreeXlocImu
///
public static xloc_imu_t ToXlocImu(this Imu imu)
{
// Validate and normalize quaternion
var quat = imu.Orientation;
double quatLength = Math.Sqrt(quat.X * quat.X + quat.Y * quat.Y + quat.Z * quat.Z + quat.W * quat.W);
if (quatLength < 0.001 || double.IsNaN(quatLength) || double.IsInfinity(quatLength))
{
// Invalid quaternion, use identity
Console.WriteLine("[XLOC] Invalid IMU quaternion detected, using identity");
quat = new RobotNet10.Shared.Geometry.Quaternion { X = 0, Y = 0, Z = 0, W = 1 };
quatLength = 1.0;
}
// Normalize quaternion
quat = new RobotNet10.Shared.Geometry.Quaternion
{
X = quat.X / quatLength,
Y = quat.Y / quatLength,
Z = quat.Z / quatLength,
W = quat.W / quatLength
};
// Validate angular velocity (clamp extremely large values)
const double MAX_ANGULAR_VEL = 10.0; // rad/s
var angVel = imu.AngularVelocity;
if (Math.Abs(angVel.X) > MAX_ANGULAR_VEL || Math.Abs(angVel.Y) > MAX_ANGULAR_VEL ||
Math.Abs(angVel.Z) > MAX_ANGULAR_VEL ||
double.IsNaN(angVel.X) || double.IsNaN(angVel.Y) || double.IsNaN(angVel.Z))
{
Console.WriteLine($"[XLOC] Invalid angular velocity detected: ({angVel.X}, {angVel.Y}, {angVel.Z}), clamping");
angVel = new Vector3
{
X = Math.Clamp(double.IsNaN(angVel.X) ? 0 : angVel.X, -MAX_ANGULAR_VEL, MAX_ANGULAR_VEL),
Y = Math.Clamp(double.IsNaN(angVel.Y) ? 0 : angVel.Y, -MAX_ANGULAR_VEL, MAX_ANGULAR_VEL),
Z = Math.Clamp(double.IsNaN(angVel.Z) ? 0 : angVel.Z, -MAX_ANGULAR_VEL, MAX_ANGULAR_VEL)
};
}
var xlocImu = new xloc_imu_t
{
header = imu.Header.ToXlocHeader(),
orientation = new double[4]
{
quat.X,
quat.Y,
quat.Z,
quat.W
},
orientation_covariance = imu.OrientationCovariance ?? new double[9],
angular_velocity = new double[3]
{
angVel.X,
angVel.Y,
angVel.Z
},
angular_velocity_covariance = imu.AngularVelocityCovariance ?? new double[9],
linear_acceleration = new double[3]
{
imu.LinearAcceleration.X,
imu.LinearAcceleration.Y,
imu.LinearAcceleration.Z
},
linear_acceleration_covariance = imu.LinearAccelerationCovariance ?? new double[9]
};
return xlocImu;
}
///
/// Convert C# LaserScan to xloc_laserscan_t
/// NOTE: Caller must free all allocated memory using FreeXlocLaserScan
///
public static xloc_laserscan_t ToXlocLaserScan(this LaserScan scan)
{
var xlocScan = new xloc_laserscan_t
{
header = scan.Header.ToXlocHeader(),
angle_min = (float)scan.AngleMin,
angle_max = (float)scan.AngleMax,
angle_increment = (float)scan.AngleIncrement,
time_increment = (float)scan.TimeIncrement,
scan_time = (float)scan.ScanTime,
range_min = (float)scan.RangeMin,
range_max = (float)scan.RangeMax,
ranges_length = (nuint)(scan.Ranges?.Length ?? 0),
intensities_length = 0
};
// Allocate and copy ranges array with validation
if (scan.Ranges != null && scan.Ranges.Length > 0)
{
// Sanitize ranges: replace NaN/Infinity/negative with max range
var sanitizedRanges = new float[scan.Ranges.Length];
int invalidCount = 0;
for (int i = 0; i < scan.Ranges.Length; i++)
{
float range = (float)scan.Ranges[i];
if (float.IsNaN(range) || float.IsInfinity(range) || range < 0)
{
sanitizedRanges[i] = (float)scan.RangeMax;
invalidCount++;
}
else
{
sanitizedRanges[i] = range;
}
}
if (invalidCount > 0)
{
Console.WriteLine($"[XLOC] Sanitized {invalidCount}/{scan.Ranges.Length} invalid laser scan ranges");
}
int rangesSize = sanitizedRanges.Length * sizeof(float);
xlocScan.ranges = Marshal.AllocHGlobal(rangesSize);
Marshal.Copy(sanitizedRanges, 0, xlocScan.ranges, sanitizedRanges.Length);
}
else
{
xlocScan.ranges = IntPtr.Zero;
}
// Intensities are optional; when provided they should match ranges count.
if (scan.Intensities != null && scan.Intensities.Length > 0)
{
int expectedLength = scan.Ranges?.Length ?? 0;
if (scan.Intensities.Length == expectedLength)
{
var sanitizedIntensities = new float[scan.Intensities.Length];
for (int i = 0; i < scan.Intensities.Length; i++)
{
float intensity = (float)scan.Intensities[i];
sanitizedIntensities[i] = (float.IsNaN(intensity) || float.IsInfinity(intensity)) ? 0f : intensity;
}
int intensitiesSize = sanitizedIntensities.Length * sizeof(float);
xlocScan.intensities = Marshal.AllocHGlobal(intensitiesSize);
Marshal.Copy(sanitizedIntensities, 0, xlocScan.intensities, sanitizedIntensities.Length);
xlocScan.intensities_length = (nuint)sanitizedIntensities.Length;
}
else
{
Console.WriteLine($"[XLOC] Ignoring intensities due to length mismatch: ranges={expectedLength}, intensities={scan.Intensities.Length}");
xlocScan.intensities = IntPtr.Zero;
xlocScan.intensities_length = 0;
}
}
else
{
xlocScan.intensities = IntPtr.Zero;
xlocScan.intensities_length = 0;
}
return xlocScan;
}
#region Memory Management
///
/// Free memory allocated for xloc_odometry_t
///
public static void FreeXlocOdometry(ref xloc_odometry_t odom)
{
if (odom.header.frame_id != IntPtr.Zero)
{
Marshal.FreeHGlobal(odom.header.frame_id);
odom.header.frame_id = IntPtr.Zero;
}
if (odom.child_frame_id != IntPtr.Zero)
{
Marshal.FreeHGlobal(odom.child_frame_id);
odom.child_frame_id = IntPtr.Zero;
}
}
///
/// Free memory allocated for xloc_imu_t
///
public static void FreeXlocImu(ref xloc_imu_t imu)
{
if (imu.header.frame_id != IntPtr.Zero)
{
Marshal.FreeHGlobal(imu.header.frame_id);
imu.header.frame_id = IntPtr.Zero;
}
}
///
/// Free memory allocated for xloc_laserscan_t
///
public static void FreeXlocLaserScan(ref xloc_laserscan_t scan)
{
if (scan.header.frame_id != IntPtr.Zero)
{
Marshal.FreeHGlobal(scan.header.frame_id);
scan.header.frame_id = IntPtr.Zero;
}
if (scan.ranges != IntPtr.Zero)
{
Marshal.FreeHGlobal(scan.ranges);
scan.ranges = IntPtr.Zero;
}
if (scan.intensities != IntPtr.Zero)
{
Marshal.FreeHGlobal(scan.intensities);
scan.intensities = IntPtr.Zero;
}
}
///
/// Get managed string from xloc status response and free the C string
///
public static string GetMessageAndFree(ref xloc_status_response_t response)
{
if (response.message == IntPtr.Zero)
return string.Empty;
string message = Marshal.PtrToStringUTF8(response.message) ?? string.Empty;
XlocNativeInterface.xloc_free_cstring(response.message);
response.message = IntPtr.Zero;
return message;
}
#endregion
}