using RobotNet10.Shared.Sensor;
using System.Runtime.InteropServices;
namespace RobotNet10.RobotApp.Xloc;
///
/// High-level wrapper for xloc localization library
/// Provides thread-safe access to xloc functionality with automatic resource management
///
public class XlocClient : IDisposable
{
private volatile IntPtr _handle = IntPtr.Zero;
// public IntPtr _tfBuffer = IntPtr.Zero; // TF3 buffer for xloc
private readonly object _lock = new object();
private readonly object _dispatchLock = new object();
private bool _disposed = false;
private readonly ILogger? _logger;
private IntPtr _tfBuffer = IntPtr.Zero;
///
/// Public property to access the TF3 buffer for use in navigation and other components
///
public IntPtr TfBuffer => _tfBuffer;
public XlocClient(IntPtr tfBuffer, ILogger? logger = null)
{
_logger = logger;
_tfBuffer = tfBuffer; // TF3 buffer for xloc
}
///
/// Initialize xloc instance
/// Note: TF3BufferManager must be initialized before calling this method
///
public void Initialize()
{
lock (_lock)
{
if (_handle != IntPtr.Zero)
{
_logger?.LogWarning("XlocClient already initialized");
return;
}
try
{
// Verify TF buffer is available from TF3BufferManager
if (_tfBuffer == IntPtr.Zero)
{
throw new InvalidOperationException("TF buffer not initialized. Ensure TF3BufferManager is initialized first.");
}
// _logger?.LogInformation("[XLOC] ═══════════════════════════════════════════");
// _logger?.LogInformation("[XLOC] Initializing XlocClient...");
// _logger?.LogInformation("[XLOC] TF buffer available: {Buffer}", _tfBuffer);
// Wait a moment to ensure TF3 transforms are fully propagated
// This is critical - the native xloc library needs time to index the transforms
_logger?.LogInformation("[XLOC] Waiting for TF3 transforms to be ready (500ms)...");
System.Threading.Thread.Sleep(500);
// _logger?.LogInformation("[XLOC] Creating xloc instance with TF buffer...");
// Create xloc instance with TF buffer (initialized and populated with static transforms)
_handle = XlocNativeInterface.xloc_create(_tfBuffer);
_logger?.LogInformation("[XLOC] xloc_create() returned handle: {Handle}", _handle);
if (_handle == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create xloc instance. xloc_create returned null.");
}
// _logger?.LogInformation("[XLOC] ═══════════════════════════════════════════");
// _logger?.LogInformation("[XLOC] ✅ XlocClient initialized successfully");
// _logger?.LogInformation("[XLOC] Ready to serve: localization, mapping, odometry dispatch");
}
catch (Exception ex)
{
_logger?.LogError(ex, "[XLOC] ❌ Exception in Initialize: {Message}", ex.Message);
throw;
}
}
}
///
/// Check if xloc is initialized
///
public bool IsInitialized
{
get
{
lock (_lock)
{
return _handle != IntPtr.Zero;
}
}
}
///
/// Dispatch Odometry data to xloc
///
public void DispatchOdometry(Odometry odom, string sensorId)
{
ThrowIfNotInitializedFast();
xloc_odometry_t xlocOdom = default;
try
{
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
_logger?.LogTrace("[{Time}] [DISPATCH-START] Odometry from {SensorId}", timestamp, sensorId);
xlocOdom = odom.ToXlocOdometry();
lock (_dispatchLock)
{
_logger?.LogTrace("[{Time}] [NATIVE-CALL] xloc_dispatch_odometry...", timestamp);
XlocNativeInterface.xloc_dispatch_odometry(_handle, sensorId, ref xlocOdom);
_logger?.LogTrace("[{Time}] [NATIVE-DONE] xloc_dispatch_odometry OK", timestamp);
}
_logger?.LogTrace("[{Time}] [DISPATCH-DONE] Odometry from {SensorId}", timestamp, sensorId);
}
catch (Exception ex)
{
_logger?.LogError(ex, "[DISPATCH-ERROR] Odometry dispatch failed: {Message}", ex.Message);
throw;
}
finally
{
// Free allocated memory
XlocConversionExtensions.FreeXlocOdometry(ref xlocOdom);
}
}
///
/// Dispatch IMU data to xloc
///
public void DispatchImu(Imu imu, string sensorId)
{
ThrowIfNotInitializedFast();
xloc_imu_t xlocImu = default;
try
{
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
_logger?.LogTrace("[{Time}] [DISPATCH-START] IMU from {SensorId}", timestamp, sensorId);
xlocImu = imu.ToXlocImu();
lock (_dispatchLock)
{
_logger?.LogTrace("[{Time}] [NATIVE-CALL] xloc_dispatch_imu...", timestamp);
XlocNativeInterface.xloc_dispatch_imu(_handle, sensorId, ref xlocImu);
_logger?.LogTrace("[{Time}] [NATIVE-DONE] xloc_dispatch_imu OK", timestamp);
}
_logger?.LogTrace("[{Time}] [DISPATCH-DONE] IMU from {SensorId}", timestamp, sensorId);
}
catch (Exception ex)
{
_logger?.LogError(ex, "[DISPATCH-ERROR] IMU dispatch failed: {Message}", ex.Message);
throw;
}
finally
{
// Free allocated memory
XlocConversionExtensions.FreeXlocImu(ref xlocImu);
}
}
///
/// Dispatch LaserScan data to xloc
///
public void DispatchLaserScan(LaserScan scan, string sensorId)
{
ThrowIfNotInitializedFast();
if (string.IsNullOrWhiteSpace(sensorId))
{
throw new ArgumentException("Sensor ID must not be null or empty.", nameof(sensorId));
}
if (scan.Ranges == null || scan.Ranges.Length == 0)
{
_logger?.LogWarning("[DISPATCH-SKIP] LaserScan from {SensorId} has no ranges", sensorId);
return;
}
xloc_laserscan_t xlocScan = default;
try
{
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
xlocScan = scan.ToXlocLaserScan();
var lockWaitStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
lock (_dispatchLock)
{
var lockWaitMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - lockWaitStart;
var nativeStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
XlocNativeInterface.xloc_dispatch_laserscan(_handle, sensorId, ref xlocScan);
var nativeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - nativeStart;
if (lockWaitMs > 5 || nativeMs > 10)
{
// _logger?.LogWarning(
// "[XLOC-DIAG] DispatchLaserScan {SensorId} lock_wait={LockWaitMs}ms native={NativeMs}ms",
// sensorId, lockWaitMs, nativeMs);
}
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "[DISPATCH-ERROR] LaserScan dispatch failed: {Message}", ex.Message);
throw;
}
finally
{
// Free allocated memory
XlocConversionExtensions.FreeXlocLaserScan(ref xlocScan);
}
}
///
/// Activate a map file
///
public bool ActivateMap(string mapFileName)
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_activate_map(_handle, mapFileName);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Map activated successfully: {MapFile}", mapFileName);
return true;
}
else
{
_logger?.LogWarning("Failed to activate map {MapFile}: Code={Code}, Message={Message}",
mapFileName, response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error activating map {MapFile}", mapFileName);
throw;
}
}
///
/// Start localization
/// Automatically resets SLAM error state before starting to clear any previous trajectory state
///
public bool StartLocalization()
{
ThrowIfNotInitialized();
try
{
// Reset SLAM error state first to clear any previous trajectory state
// This is required when starting a new localization session after stopping a previous one
_logger?.LogInformation("Resetting SLAM error state before starting localization...");
ResetSlamError();
// Small delay to ensure reset is processed
System.Threading.Thread.Sleep(100);
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_start_localization(_handle);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Localization started successfully");
return true;
}
else
{
_logger?.LogWarning("Failed to start localization: Code={Code}, Message={Message}",
response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error starting localization");
throw;
}
}
///
/// Stop localization
///
public bool StopLocalization()
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_stop_localization(_handle);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Localization stopped successfully");
return true;
}
else
{
_logger?.LogWarning("Failed to stop localization: Code={Code}, Message={Message}",
response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error stopping localization");
throw;
}
}
///
/// Start mapping
/// Automatically resets SLAM error state before starting to clear any previous trajectory state
///
public bool StartMapping()
{
ThrowIfNotInitialized();
try
{
// Reset SLAM error state first to clear any previous trajectory state
// This is required when starting a new mapping session after stopping a previous one
_logger?.LogInformation("Resetting SLAM error state before starting mapping...");
ResetSlamError();
// Small delay to ensure reset is processed
System.Threading.Thread.Sleep(100);
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_start_mapping(_handle);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Mapping started successfully");
return true;
}
else
{
_logger?.LogWarning("Failed to start mapping: Code={Code}, Message={Message}",
response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error starting mapping");
throw;
}
}
///
/// Stop mapping and save to file
///
public bool StopMapping(string mapFileName)
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_stop_mapping(_handle, mapFileName);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Mapping stopped and saved to {MapFile}", mapFileName);
return true;
}
else
{
_logger?.LogWarning("Failed to stop mapping: Code={Code}, Message={Message}",
response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error stopping mapping");
throw;
}
}
///
/// Switch to a different map with optional initial pose
///
public bool SwitchMap(string mapFileName, (double x, double y, double z, double qx, double qy, double qz, double qw)? initialPose = null)
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
if (initialPose.HasValue)
{
var pose = new xloc_pose_t
{
position = new double[] { initialPose.Value.x, initialPose.Value.y, initialPose.Value.z },
orientation = new double[] { initialPose.Value.qx, initialPose.Value.qy, initialPose.Value.qz, initialPose.Value.qw }
};
response = XlocNativeInterface.xloc_switch_map(_handle, mapFileName, 1, ref pose);
}
else
{
var dummyPose = new xloc_pose_t();
response = XlocNativeInterface.xloc_switch_map(_handle, mapFileName, 0, ref dummyPose);
}
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Switched to map: {MapFile}", mapFileName);
return true;
}
else
{
_logger?.LogWarning("Failed to switch map: Code={Code}, Message={Message}", response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error switching map");
throw;
}
}
///
/// Change map origin (re-center the map coordinate system)
///
public bool ChangeMapOrigin(double x, double y, double z, double qx, double qy, double qz, double qw)
{
ThrowIfNotInitialized();
try
{
var newOrigin = new xloc_pose_t
{
position = new double[] { x, y, z },
orientation = new double[] { qx, qy, qz, qw }
};
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_change_map_origin(_handle, ref newOrigin);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Map origin changed to: ({X}, {Y}, {Z})", x, y, z);
return true;
}
else
{
_logger?.LogWarning("Failed to change map origin: Code={Code}, Message={Message}", response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error changing map origin");
throw;
}
}
///
/// Start updating existing map (allow SLAM to modify the loaded map)
///
public bool StartUpdateMap()
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_start_update_map(_handle);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Started updating map");
return true;
}
else
{
_logger?.LogWarning("Failed to start map update: Code={Code}, Message={Message}", response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error starting map update");
throw;
}
}
///
/// Stop updating map and optionally save the updated map
///
public bool StopUpdateMap(bool saveUpdatedMap = true)
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_stop_update_map(_handle, saveUpdatedMap ? 1 : 0);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Stopped updating map (save={Save})", saveUpdatedMap);
return true;
}
else
{
_logger?.LogWarning("Failed to stop map update: Code={Code}, Message={Message}", response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error stopping map update");
throw;
}
}
///
/// Reset SLAM error state (attempt recovery from error)
///
public bool ResetSlamError()
{
ThrowIfNotInitialized();
try
{
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_reset_slam_error(_handle);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("SLAM error reset successfully");
return true;
}
else
{
_logger?.LogWarning("Failed to reset SLAM error: Code={Code}, Message={Message}", response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error resetting SLAM error");
throw;
}
}
///
/// Set initial pose for localization
///
public bool SetInitialPose(double x, double y, double z, double qx, double qy, double qz, double qw)
{
ThrowIfNotInitialized();
try
{
// Get map origin BEFORE setting initial pose
var mapBefore = GetStaticGridMap(reloadFromFile: false);
_logger?.LogWarning("=== XlocClient.SetInitialPose - BEFORE ===");
if (mapBefore != null)
{
_logger?.LogWarning("Map origin BEFORE: ({X}, {Y}, {Z})",
mapBefore.Origin.X, mapBefore.Origin.Y, mapBefore.Origin.Z);
}
else
{
_logger?.LogWarning("Map origin BEFORE: NULL (no map available)");
}
var pose = new xloc_pose_t
{
position = new double[] { x, y, z },
orientation = new double[] { qx, qy, qz, qw }
};
_logger?.LogWarning("Calling xloc_set_initial_pose: ({X}, {Y}, {Z}) Q:({Qx}, {Qy}, {Qz}, {Qw})",
x, y, z, qx, qy, qz, qw);
xloc_status_response_t response;
lock (_lock)
{
response = XlocNativeInterface.xloc_set_initial_pose(_handle, ref pose);
}
string message = XlocConversionExtensions.GetMessageAndFree(ref response);
if (response.code == 0)
{
_logger?.LogInformation("Initial pose set to: ({X}, {Y}, {Z})", x, y, z);
// Get map origin AFTER setting initial pose
System.Threading.Thread.Sleep(50); // Small delay to let XLOC process
var mapAfter = GetStaticGridMap(reloadFromFile: false);
_logger?.LogWarning("=== XlocClient.SetInitialPose - AFTER ===");
if (mapAfter != null)
{
_logger?.LogWarning("Map origin AFTER: ({X}, {Y}, {Z})",
mapAfter.Origin.X, mapAfter.Origin.Y, mapAfter.Origin.Z);
// Compare with before
if (mapBefore != null)
{
var dx = Math.Abs(mapBefore.Origin.X - mapAfter.Origin.X);
var dy = Math.Abs(mapBefore.Origin.Y - mapAfter.Origin.Y);
var dz = Math.Abs(mapBefore.Origin.Z - mapAfter.Origin.Z);
if (dx > 0.001 || dy > 0.001 || dz > 0.001)
{
_logger?.LogError("⚠️ CRITICAL ERROR: Map origin changed after xloc_set_initial_pose!");
_logger?.LogError(" Before: ({X}, {Y}, {Z})", mapBefore.Origin.X, mapBefore.Origin.Y, mapBefore.Origin.Z);
_logger?.LogError(" After: ({X}, {Y}, {Z})", mapAfter.Origin.X, mapAfter.Origin.Y, mapAfter.Origin.Z);
_logger?.LogError(" Difference: ΔX={DX:F3}m, ΔY={DY:F3}m, ΔZ={DZ:F3}m", dx, dy, dz);
}
else
{
_logger?.LogWarning("✓ Map origin unchanged after xloc_set_initial_pose ✓");
}
}
}
else
{
_logger?.LogWarning("Map origin AFTER: NULL (no map available)");
}
return true;
}
else
{
_logger?.LogWarning("Failed to set initial pose: Code={Code}, Message={Message}", response.code, message);
return false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error setting initial pose");
throw;
}
}
///
/// Get current pose estimate from xloc
///
public (double x, double y, double z, double qx, double qy, double qz, double qw)? GetCurrentPose()
{
ThrowIfNotInitialized();
IntPtr posePtr = IntPtr.Zero;
try
{
_logger?.LogTrace("[DEBUG] About to call xloc_get_current_pose()...");
lock (_lock)
{
posePtr = XlocNativeInterface.xloc_get_current_pose(_handle);
}
_logger?.LogTrace("[DEBUG] xloc_get_current_pose() returned: {PosePtr}", posePtr);
if (posePtr == IntPtr.Zero)
{
_logger?.LogWarning("xloc_get_current_pose returned null");
return null;
}
_logger?.LogTrace("[DEBUG] Marshaling pose structure...");
// Marshal the pose from unmanaged memory
var pose = Marshal.PtrToStructure(posePtr);
_logger?.LogTrace("[DEBUG] Pose marshaled successfully");
return (
pose.position[0], pose.position[1], pose.position[2],
pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]
);
}
catch (Exception ex)
{
_logger?.LogError(ex, "[DEBUG] Exception in GetCurrentPose: {Message}", ex.Message);
return null;
}
finally
{
if (posePtr != IntPtr.Zero)
XlocNativeInterface.xloc_free_pose(posePtr);
}
}
///
/// Get static grid map (from loaded map file)
///
public OccupancyGridData? GetStaticGridMap(bool reloadFromFile = false)
{
ThrowIfNotInitialized();
IntPtr gridPtr = IntPtr.Zero;
try
{
// _logger?.LogTrace("[DEBUG] About to call xloc_get_static_grid_map()...");
lock (_lock)
{
gridPtr = XlocNativeInterface.xloc_get_static_grid_map(_handle, reloadFromFile ? 1 : 0);
}
// _logger?.LogTrace("[DEBUG] xloc_get_static_grid_map() returned: {GridPtr}", gridPtr);
if (gridPtr == IntPtr.Zero)
{
// _logger?.LogWarning("xloc_get_static_grid_map returned null");
return null;
}
// _logger?.LogTrace("[DEBUG] Marshaling occupancy grid structure...");
var grid = Marshal.PtrToStructure(gridPtr);
// _logger?.LogInformation("[XLOC] GetStaticGridMap marshaled: width={Width}, height={Height}, resolution={Resolution}, data_length={DataLength}, data_ptr={DataPtr}",
// grid.width, grid.height, grid.resolution, grid.data_length, grid.data);
// Marshal frame_id string
string frameId = string.Empty;
if (grid.header.frame_id != IntPtr.Zero)
{
frameId = Marshal.PtrToStringAnsi(grid.header.frame_id) ?? string.Empty;
}
// Copy occupancy data
byte[] data = new byte[grid.data_length];
if (grid.data != IntPtr.Zero && grid.data_length > 0)
{
Marshal.Copy(grid.data, data, 0, (int)grid.data_length);
}
var result = new OccupancyGridData
{
Resolution = grid.resolution,
Width = grid.width,
Height = grid.height,
Origin = new PoseData
{
X = grid.origin.position[0],
Y = grid.origin.position[1],
Z = grid.origin.position[2],
Qx = grid.origin.orientation[0],
Qy = grid.origin.orientation[1],
Qz = grid.origin.orientation[2],
Qw = grid.origin.orientation[3]
},
Data = data,
FrameId = frameId
};
// _logger?.LogInformation("[XLOC] GetStaticGridMap returning: width={Width}, height={Height}, resolution={Resolution}, data_length={DataLength}",
// result.Width, result.Height, result.Resolution, result.Data?.Length ?? 0);
return result;
}
catch (Exception ex)
{
_logger?.LogError(ex, "[DEBUG] Exception in GetStaticGridMap: {Message}", ex.Message);
return null;
}
finally
{
if (gridPtr != IntPtr.Zero)
XlocNativeInterface.xloc_free_occupancy_grid(gridPtr);
}
}
///
/// Get static grid map pointer (from loaded map file)
/// Returns IntPtr to xloc_occupancy_grid_t (must be freed with xloc_free_occupancy_grid)
///
public IntPtr GetStaticGridMapPtr(bool reloadFromFile = false)
{
ThrowIfNotInitialized();
lock (_lock)
{
return XlocNativeInterface.xloc_get_static_grid_map(_handle, reloadFromFile ? 1 : 0);
}
}
///
/// Get online grid map pointer (from SLAM)
/// Returns IntPtr to xloc_occupancy_grid_t (must be freed with xloc_free_occupancy_grid)
///
public IntPtr GetOnlineGridMapPtr()
{
ThrowIfNotInitialized();
lock (_lock)
{
return XlocNativeInterface.xloc_get_online_grid_map(_handle);
}
}
///
/// Get online grid map (from SLAM)
///
public OccupancyGridData? GetOnlineGridMap()
{
ThrowIfNotInitialized();
IntPtr gridPtr = IntPtr.Zero;
try
{
_logger?.LogTrace("[DEBUG] About to call xloc_get_online_grid_map()...");
lock (_lock)
{
gridPtr = XlocNativeInterface.xloc_get_online_grid_map(_handle);
}
_logger?.LogTrace("[DEBUG] xloc_get_online_grid_map() returned: {GridPtr}", gridPtr);
if (gridPtr == IntPtr.Zero)
{
_logger?.LogWarning("xloc_get_online_grid_map returned null");
return null;
}
_logger?.LogTrace("[DEBUG] Marshaling occupancy grid structure...");
var grid = Marshal.PtrToStructure(gridPtr);
_logger?.LogTrace("[DEBUG] Occupancy grid marshaled successfully");
// Marshal frame_id string
string frameId = string.Empty;
if (grid.header.frame_id != IntPtr.Zero)
{
frameId = Marshal.PtrToStringAnsi(grid.header.frame_id) ?? string.Empty;
}
// Copy occupancy data
byte[] data = new byte[grid.data_length];
if (grid.data != IntPtr.Zero && grid.data_length > 0)
{
Marshal.Copy(grid.data, data, 0, (int)grid.data_length);
}
return new OccupancyGridData
{
Resolution = grid.resolution,
Width = grid.width,
Height = grid.height,
Origin = new PoseData
{
X = grid.origin.position[0],
Y = grid.origin.position[1],
Z = grid.origin.position[2],
Qx = grid.origin.orientation[0],
Qy = grid.origin.orientation[1],
Qz = grid.origin.orientation[2],
Qw = grid.origin.orientation[3]
},
Data = data,
FrameId = frameId
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "[DEBUG] Exception in GetOnlineGridMap: {Message}", ex.Message);
return null;
}
finally
{
if (gridPtr != IntPtr.Zero)
XlocNativeInterface.xloc_free_occupancy_grid(gridPtr);
}
}
public XlocDiagnosticsData? GetDiagnostics()
{
ThrowIfNotInitialized();
IntPtr diagPtr = IntPtr.Zero;
try
{
diagPtr = XlocNativeInterface.xloc_get_diagnostics(_handle);
if (diagPtr == IntPtr.Zero)
{
_logger?.LogWarning("xloc_get_diagnostics returned null");
return null;
}
var diag = Marshal.PtrToStructure(diagPtr);
// Marshal map name string
string mapName = string.Empty;
if (diag.current_active_map != IntPtr.Zero)
{
mapName = Marshal.PtrToStringAnsi(diag.current_active_map) ?? string.Empty;
}
// Marshal frame_id string from header
string frameId = string.Empty;
if (diag.header.frame_id != IntPtr.Zero)
{
frameId = Marshal.PtrToStringAnsi(diag.header.frame_id) ?? string.Empty;
}
return new XlocDiagnosticsData
{
HeaderSeq = diag.header.seq,
HeaderStampSec = diag.header.stamp.sec,
HeaderStampNsec = diag.header.stamp.nsec,
HeaderFrameId = frameId,
XlocState = diag.xloc_state,
CurrentActiveMap = mapName,
Reliability = diag.reliability,
MatchingScore = diag.matching_score
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting diagnostics");
return null;
}
finally
{
if (diagPtr != IntPtr.Zero)
{
XlocNativeInterface.xloc_free_diagnostics(diagPtr);
}
}
}
private void ThrowIfNotInitialized()
{
lock (_lock)
{
if (_handle == IntPtr.Zero)
{
throw new InvalidOperationException("XlocClient is not initialized. Call Initialize() first.");
}
}
}
///
/// Lock-free initialization check for high-frequency dispatch paths.
/// Uses volatile read of _handle to avoid lock contention with admin operations.
///
private void ThrowIfNotInitializedFast()
{
if (_handle == IntPtr.Zero)
{
throw new InvalidOperationException("XlocClient is not initialized. Call Initialize() first.");
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
lock (_lock)
{
if (_handle != IntPtr.Zero)
{
try
{
XlocNativeInterface.xloc_destroy(_handle);
_logger?.LogInformation("XlocClient disposed successfully");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error disposing XlocClient");
}
finally
{
_handle = IntPtr.Zero;
}
}
// Note: TF buffer is managed by TF3BufferManager, do not destroy it here
// Only reset the reference
_tfBuffer = IntPtr.Zero;
}
_disposed = true;
}
~XlocClient()
{
Dispose(false);
}
}
///
/// Occupancy grid data for display
///
public class OccupancyGridData
{
public float Resolution { get; set; }
public uint Width { get; set; }
public uint Height { get; set; }
public PoseData Origin { get; set; } = new();
public byte[] Data { get; set; } = Array.Empty();
public string FrameId { get; set; } = string.Empty;
}
///
/// Pose data
///
public class PoseData
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
public double Qx { get; set; }
public double Qy { get; set; }
public double Qz { get; set; }
public double Qw { get; set; }
}
///
/// XLOC diagnostics data
///
public class XlocDiagnosticsData
{
public uint HeaderSeq { get; set; }
public uint HeaderStampSec { get; set; }
public uint HeaderStampNsec { get; set; }
public string HeaderFrameId { get; set; } = string.Empty;
public byte XlocState { get; set; } // 0=MAPPING, 1=LOCALIZATION, 2=PROCESSING, 3=READY, 4=ERROR
public string CurrentActiveMap { get; set; } = string.Empty;
public double Reliability { get; set; } // 0.0 to 1.0
public double MatchingScore { get; set; } // SLAM matching quality
public string StateString => XlocState switch
{
0 => "MAPPING",
1 => "LOCALIZATION",
2 => "PROCESSING",
3 => "READY",
4 => "ERROR",
_ => $"Unknown ({XlocState})"
};
}