Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -0,0 +1,376 @@
# XLOC Manual Control API Guide
Now you have **FULL MANUAL CONTROL** over XLOC SLAM operations! Control via:
1. C# Service Methods
2. REST API Endpoints
3. SignalR Hub Methods
---
## 1. C# Service Methods (Direct)
Inject `XlocIntegrationService` into your service:
```csharp
public class MyNavigationService
{
private readonly XlocIntegrationService _xloc;
public MyNavigationService(XlocIntegrationService xloc)
{
_xloc = xloc;
}
public void StartLocalizationWithMap()
{
// Activate map
if (_xloc.ActivateMap("/maps/factory_floor.pbstream"))
{
// Start localization
_xloc.StartLocalization();
}
}
public void BeginMapping()
{
_xloc.StartMapping();
}
public void SaveAndStopMapping()
{
_xloc.StopMapping("/maps/new_map.pbstream");
}
}
```
---
## 2. REST API Endpoints
### Activate Map
```bash
curl -X POST "https://localhost:7002/api/xloc/activate-map?mapPath=/maps/factory.pbstream"
```
Response:
```json
{
"success": true,
"message": "Map activated"
}
```
### Start Mapping
```bash
dotnet build
# Restart app
pkill -f dotnet
./run-quiet.sh
# Test mapping
curl -k -X POST https://127.0.0.1:7002/api/motion/ps5/enable
curl -k -X POST https://localhost:7002/api/xloc/mapping/start
# ... di chuyển robot ...
```
### Stop Mapping
```bash
curl -k -X POST https://localhost:7002/api/xloc/mapping/stop \
-H "Content-Type: application/json" \
-d '{"map_file_path": "test14"}'
```
### Active Map
```bash
curl -k -X POST https://localhost:7002/api/xloc/map/activate \
-H "Content-Type: application/json" \
-d '{"map_file_path": "test10"}'
```
### Start Localization
```bash
curl -k -X POST https://localhost:7002/api/xloc/localization/start
```
### Stop Localization
```bash
curl -k -X POST https://localhost:7002/api/xloc/localization/stop
```
### Reset SLAM State
```bash
# Reset SLAM error state (automatically called before start mapping/localization)
# Useful if you manually need to clear previous trajectory state
curl -k -X POST https://localhost:7002/api/xloc/slam/reset
```
**Note:** `StartMapping()` and `StartLocalization()` now automatically call reset before starting, so you typically don't need to call this manually.
### Stop Mapping & Save
**Option 1: Manual (will crash, but map is saved)**
```bash
# Save with timestamp
MAP_NAME="map_$(date +%Y%m%d_%H%M%S).pbstream"
curl -k -X POST "https://localhost:7002/api/xloc/stop-mapping?savePath=/home/robotics/sonvh/RobotNet10/srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Xloc/map/$MAP_NAME"
# Or save with custom name (MUST include .pbstream extension!)
curl -k -X POST "https://localhost:7002/api/xloc/stop-mapping?savePath=/home/robotics/sonvh/RobotNet10/srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Xloc/map/my_map.pbstream"
cd Xloc
./map-and-save.sh my_office_map.pbstream
# Note: Application will crash due to XLOC Cairo bug, but map is saved successfully
# Restart with: ./run-quiet.sh
```
**Option 2: Automated script (recommended)**
```bash
# Use automated script that handles crash and restart
cd Xloc
./map-and-save.sh my_map.pbstream
# Script will:
# 1. Wait for you to drive robot
# 2. Save map on ENTER
# 3. Handle crash gracefully
# 4. Auto-restart application
```
### Get Current Pose
```bash
curl "https://localhost:7002/api/xloc/pose"
```
Response:
```json
{
"x": 1.234,
"y": 5.678,
"yaw": 1.57,
"yawDegrees": 90.0
}
```
---
## 3. SignalR Hub Methods
### TypeScript/JavaScript Client
```typescript
import * as signalR from "@microsoft/signalr";
const connection = new signalR.HubConnectionBuilder()
.withUrl("https://localhost:7002/hubs/xloc/pose")
.build();
// Subscribe to pose updates (realtime streaming)
connection.on("ReceivePose", (pose) => {
console.log(`Position: (${pose.x}, ${pose.y}), Heading: ${pose.yawDegrees}°`);
});
await connection.start();
// Control SLAM manually
async function startLocalization() {
const success = await connection.invoke("StartLocalization");
console.log("Localization started:", success);
}
async function activateMap(mapPath: string) {
const success = await connection.invoke("ActivateMap", mapPath);
console.log("Map activated:", success);
}
async function startMapping() {
const success = await connection.invoke("StartMapping");
console.log("Mapping started:", success);
}
async function stopMapping(savePath: string) {
const success = await connection.invoke("StopMapping", savePath);
console.log("Map saved:", success);
}
async function getCurrentPose() {
const pose = await connection.invoke("GetCurrentPose2D");
console.log("Current pose:", pose);
// { x: 1.234, y: 5.678, yaw: 1.57, yawDegrees: 90.0 }
}
```
### React Example
```tsx
import { HubConnectionBuilder } from '@microsoft/signalr';
import { useState, useEffect } from 'react';
export function XlocControl() {
const [connection, setConnection] = useState(null);
const [pose, setPose] = useState(null);
useEffect(() => {
const conn = new HubConnectionBuilder()
.withUrl("https://localhost:7002/hubs/xloc/pose")
.build();
conn.on("ReceivePose", (data) => {
setPose(data);
});
conn.start();
setConnection(conn);
return () => conn.stop();
}, []);
const handleStartLocalization = async () => {
const result = await connection.invoke("StartLocalization");
console.log("Started:", result);
};
const handleStartMapping = async () => {
const result = await connection.invoke("StartMapping");
console.log("Mapping started:", result);
};
return (
<div>
<h2>XLOC Control Panel</h2>
{pose && (
<div>
<p>X: {pose.x.toFixed(2)}m</p>
<p>Y: {pose.y.toFixed(2)}m</p>
<p>Heading: {pose.yawDegrees.toFixed(1)}°</p>
</div>
)}
<button onClick={handleStartLocalization}>
Start Localization
</button>
<button onClick={handleStartMapping}>
Start Mapping
</button>
</div>
);
}
```
---
## Typical Workflows
### Workflow 1: Localization (Using Existing Map)
```bash
# 1. Activate map
POST /api/xloc/activate-map?mapPath=/maps/factory.pbstream
# 2. Start localization
POST /api/xloc/start-localization
# 3. Robot is now localizing!
# Pose updates stream automatically via SignalR
# 4. When done
POST /api/xloc/stop-localization
```
### Workflow 2: Mapping (Create New Map)
```bash
# 1. Start mapping
POST /api/xloc/start-mapping
# 2. Drive robot around
# Map is being created in realtime
# 3. Save and stop
POST /api/xloc/stop-mapping?savePath=/maps/new_building.pbstream
```
---
## Available Methods in All Interfaces
| Method | XlocIntegrationService | REST API | SignalR Hub |
|--------|----------------------|----------|-------------|
| ActivateMap | ✅ `ActivateMap(mapPath)` | ✅ `POST /api/xloc/activate-map` | ✅ `connection.invoke("ActivateMap", mapPath)` |
| StartLocalization | ✅ `StartLocalization()` | ✅ `POST /api/xloc/start-localization` | ✅ `connection.invoke("StartLocalization")` |
| StopLocalization | ✅ `StopLocalization()` | ✅ `POST /api/xloc/stop-localization` | ✅ `connection.invoke("StopLocalization")` |
| StartMapping | ✅ `StartMapping()` | ✅ `POST /api/xloc/start-mapping` | ✅ `connection.invoke("StartMapping")` |
| StopMapping | ✅ `StopMapping(savePath)` | ✅ `POST /api/xloc/stop-mapping` | ✅ `connection.invoke("StopMapping", savePath)` |
| GetCurrentPose2D | ✅ `GetCurrentPose2D()` | ✅ `GET /api/xloc/pose` | ✅ `connection.invoke("GetCurrentPose2D")` |
---
## Important Notes
**No Auto-Start**: SLAM does NOT start automatically anymore!
**Manual Control Only**: You must explicitly call start methods
**Sensor Data Streaming**: Continues automatically at 20Hz (Odom + IMU)
**Pose Streaming**: Broadcasts via SignalR at 5Hz when SLAM is running
## Configuration
```json
{
"Xloc": {
"Integration": {
"Enabled": true,
"Mode": "Mapping", // Ignored - now manual control
"UpdateRateHz": 20,
"MapFilePath": "", // Ignored - call ActivateMap() manually
"SaveMapFilePath": "/tmp/xloc_map.pbstream"
}
}
}
```
Note: `Mode` and `MapFilePath` in config are now ignored. Use manual control methods instead!
---
## Troubleshooting
### Cannot start mapping after stop & save
**Problem:** After stopping localization or mapping, `StartMapping()` fails.
**Root Cause:** XLOC library retains the finished trajectory state. Starting a new mapping/localization session requires clearing this state.
**Solution (Automatic):** `StartMapping()` and `StartLocalization()` now automatically call `ResetSlamError()` before starting, which clears the previous trajectory state.
**Manual Reset (if needed):**
```bash
# If automatic reset doesn't work, manually reset SLAM state
curl -k -X POST https://localhost:7002/api/xloc/slam/reset
# Then try starting mapping again
curl -k -X POST https://localhost:7002/api/xloc/mapping/start
```
**What the fix does:**
- Clears finished trajectory (trajectory ID from previous session)
- Resets SLAM error state to Idle
- Prepares XLOC library for new mapping/localization session
### General Workflow After Fix
```bash
# 1. Stop previous session (if any)
curl -k -X POST https://localhost:7002/api/xloc/localization/stop
# 2. Start mapping (automatic reset happens internally)
curl -k -X POST https://localhost:7002/api/xloc/mapping/start
# 3. Drive robot around...
# 4. Stop and save
curl -k -X POST https://localhost:7002/api/xloc/mapping/stop \
-H "Content-Type: application/json" \
-d '{"map_file_path": "my_new_map"}'
# 5. Start mapping again (works now!)
curl -k -X POST https://localhost:7002/api/xloc/mapping/start
```

View File

@@ -0,0 +1,199 @@
using System.Threading.Channels;
namespace RobotNet10.RobotApp.Xloc;
/// <summary>
/// Single-thread async dispatcher for xloc sensor data — mirrors ROS ros::spin() model.
/// All sensor types (IMU, Odom, Scan) are dispatched sequentially by ONE worker thread,
/// eliminating lock contention on the native xloc library.
///
/// ROS model: ros::spin() processes all callbacks in a single thread, no mutex needed.
/// This dispatcher replicates that pattern: one queue, one worker, zero lock contention.
/// </summary>
public class XlocAsyncDispatcher : IDisposable
{
private readonly XlocClient _xlocClient;
private readonly ILogger? _logger;
// Single unified queue for ALL sensor types — like ROS callback queue
private readonly Channel<SensorDispatchEvent> _dispatchQueue;
private readonly Task _workerTask;
private readonly CancellationTokenSource _cts;
private bool _disposed = false;
private const int DefaultQueueCapacity = 300;
private readonly int _queueCapacity;
private long _dropCount;
/// <summary>
/// Sensor dispatch event wrapper
/// </summary>
private record SensorDispatchEvent(
string SensorType,
string SensorId,
Action DispatchAction,
long EnqueueTimestamp);
public XlocAsyncDispatcher(
XlocClient xlocClient,
int queueCapacity = DefaultQueueCapacity,
ILogger? logger = null)
{
_xlocClient = xlocClient ?? throw new ArgumentNullException(nameof(xlocClient));
_logger = logger;
_queueCapacity = queueCapacity;
_dispatchQueue = Channel.CreateBounded<SensorDispatchEvent>(new BoundedChannelOptions(queueCapacity)
{
FullMode = BoundedChannelFullMode.DropOldest
});
_cts = new CancellationTokenSource();
_workerTask = RunWorker(_cts.Token);
}
public ValueTask EnqueueOdometryAsync(Action dispatchAction, string sensorId = "odom")
{
ThrowIfDisposed();
return EnqueueAsync("odometry", sensorId, dispatchAction);
}
public ValueTask EnqueueImuAsync(Action dispatchAction, string sensorId = "imu")
{
ThrowIfDisposed();
return EnqueueAsync("imu", sensorId, dispatchAction);
}
public ValueTask EnqueueLaserScanAsync(Action dispatchAction, string sensorId)
{
ThrowIfDisposed();
return EnqueueAsync("laserscan", sensorId, dispatchAction);
}
private ValueTask EnqueueAsync(string sensorType, string sensorId, Action dispatchAction)
{
var enqueueTs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var @event = new SensorDispatchEvent(sensorType, sensorId, dispatchAction, enqueueTs);
if (_dispatchQueue.Reader.Count >= _queueCapacity)
{
Interlocked.Increment(ref _dropCount);
}
try
{
return _dispatchQueue.Writer.WriteAsync(@event, _cts.Token);
}
catch (ChannelClosedException)
{
throw new InvalidOperationException("XlocAsyncDispatcher has been disposed.");
}
}
private async Task RunWorker(CancellationToken ct)
{
_logger?.LogInformation("[XLOC-ASYNC] Single dispatch worker started (ROS spin model)");
try
{
await foreach (var @event in _dispatchQueue.Reader.ReadAllAsync(ct))
{
ExecuteDispatchEvent(@event);
}
}
catch (OperationCanceledException)
{
_logger?.LogInformation("[XLOC-ASYNC] Dispatch worker cancelled");
}
catch (Exception ex)
{
_logger?.LogError(ex, "[XLOC-ASYNC-FATAL] Dispatch worker crashed: {Message}", ex.Message);
}
finally
{
_logger?.LogInformation("[XLOC-ASYNC] Dispatch worker stopped");
}
}
private void ExecuteDispatchEvent(SensorDispatchEvent @event)
{
var queueWaitMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - @event.EnqueueTimestamp;
try
{
var dispatchStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
@event.DispatchAction();
var dispatchElapsedMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - dispatchStart;
_logger?.LogTrace(
"[XLOC-ASYNC] {SensorType} sensor={SensorId} queue_wait={QueueWaitMs}ms dispatch={DispatchMs}ms",
@event.SensorType,
@event.SensorId,
queueWaitMs,
dispatchElapsedMs);
if (queueWaitMs >= 10 || dispatchElapsedMs >= 10)
{
// _logger?.LogWarning(
// "[XLOC-DIAG] AsyncDispatcher {SensorType} sensor={SensorId} queue_wait={QueueWaitMs}ms dispatch={DispatchMs}ms",
// @event.SensorType,
// @event.SensorId,
// queueWaitMs,
// dispatchElapsedMs);
}
}
catch (Exception ex)
{
_logger?.LogError(ex,
"[XLOC-ASYNC-ERROR] Failed to dispatch {SensorType} sensor={SensorId}: {Message}",
@event.SensorType,
@event.SensorId,
ex.Message);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
_logger?.LogInformation("[XLOC-ASYNC] Shutting down dispatcher");
_dispatchQueue.Writer.TryComplete();
_cts.Cancel();
try
{
_workerTask.Wait(TimeSpan.FromSeconds(5));
}
catch (AggregateException ex)
{
_logger?.LogWarning(ex, "[XLOC-ASYNC] Worker task did not complete gracefully");
}
_cts.Dispose();
_disposed = true;
_logger?.LogInformation("[XLOC-ASYNC] Dispatcher shut down");
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new InvalidOperationException("XlocAsyncDispatcher has been disposed.");
}
~XlocAsyncDispatcher()
{
Dispose(false);
}
}

View File

@@ -0,0 +1,110 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace RobotNet10.RobotApp.Xloc;
/// <summary>
/// Polls XLOC diagnostics and calls <see cref="XlocIntegrationService.StartLocalization"/> when
/// state transitions to READY (3) with an active map. Does not depend on the web UI.
/// </summary>
public sealed class XlocAutoLocalizationHostedService : BackgroundService
{
private readonly XlocIntegrationService _xloc;
private readonly ILogger<XlocAutoLocalizationHostedService> _logger;
private readonly XlocIntegrationConfiguration _config = new();
private byte? _previousXlocState;
private bool _hasAutoStartedOnce;
public XlocAutoLocalizationHostedService(
XlocIntegrationService xloc,
IConfiguration configuration,
ILogger<XlocAutoLocalizationHostedService> logger)
{
_xloc = xloc;
_logger = logger;
var section = configuration.GetSection("Xloc:Integration");
if (section.Exists())
section.Bind(_config);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!_config.Enabled)
{
_logger.LogInformation("XlocAutoLocalizationHostedService skipped: Xloc:Integration:Enabled is false");
return;
}
if (!_config.AutoStartLocalizationOnReady)
{
_logger.LogInformation("XlocAutoLocalizationHostedService skipped: AutoStartLocalizationOnReady is false");
return;
}
var pollMs = Math.Clamp(_config.AutoStartLocalizationPollIntervalMs, 100, 10_000);
var cooldownMs = Math.Max(0, _config.AutoStartLocalizationCooldownAfterStopMs);
_logger.LogInformation(
"XlocAutoLocalizationHostedService running (poll {PollMs}ms, stop cooldown {CooldownMs}ms)",
pollMs,
cooldownMs);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(pollMs, stoppingToken).ConfigureAwait(false);
var diag = _xloc.GetDiagnostics();
if (diag == null)
continue;
var s = diag.XlocState;
var transitionedToReady = s == 3 && (!_previousXlocState.HasValue || _previousXlocState.Value != 3);
_previousXlocState = s;
if (!transitionedToReady || _hasAutoStartedOnce)
continue;
if (string.IsNullOrWhiteSpace(diag.CurrentActiveMap))
{
_logger.LogDebug("Auto-start localization skipped: no active map in diagnostics");
continue;
}
if (_xloc.LastLocalizationStopUtc.HasValue)
{
var elapsedMs = (DateTime.UtcNow - _xloc.LastLocalizationStopUtc.Value).TotalMilliseconds;
if (elapsedMs < cooldownMs)
{
_logger.LogDebug(
"Auto-start localization skipped: cooldown after stop ({ElapsedMs:F0}ms < {CooldownMs}ms)",
elapsedMs,
cooldownMs);
continue;
}
}
_hasAutoStartedOnce = true;
_logger.LogInformation(
"XLOC READY with active map '{Map}'. Auto-starting localization...",
diag.CurrentActiveMap);
var ok = _xloc.StartLocalization();
if (!ok)
_logger.LogWarning("Auto-start localization failed (StartLocalization returned false)");
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in XlocAutoLocalizationHostedService loop");
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,357 @@
using System.Runtime.InteropServices;
using RobotNet10.Shared;
using RobotNet10.Shared.Geometry;
using RobotNet10.Shared.Sensor;
// using RobotNet10.Shared.Numbers;
namespace RobotNet10.RobotApp.Xloc;
/// <summary>
/// Extension methods to convert C# sensor structs to xloc C-compatible structs
/// </summary>
public static class XlocConversionExtensions
{
/// <summary>
/// Convert C# Header to xloc_header_t
/// NOTE: Caller must free FrameId using Marshal.FreeHGlobal
/// </summary>
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;
}
/// <summary>
/// Convert DateTime to xloc_unix_time_t (Unix epoch time)
/// </summary>
// 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
};
}
/// <summary>
/// Convert C# Odometry to xloc_odometry_t
/// NOTE: Caller must free frame_id and child_frame_id using FreeXlocOdometry
/// </summary>
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;
}
/// <summary>
/// Convert C# Imu to xloc_imu_t
/// NOTE: Caller must free FrameId using FreeXlocImu
/// </summary>
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;
}
/// <summary>
/// Convert C# LaserScan to xloc_laserscan_t
/// NOTE: Caller must free all allocated memory using FreeXlocLaserScan
/// </summary>
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
/// <summary>
/// Free memory allocated for xloc_odometry_t
/// </summary>
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;
}
}
/// <summary>
/// Free memory allocated for xloc_imu_t
/// </summary>
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;
}
}
/// <summary>
/// Free memory allocated for xloc_laserscan_t
/// </summary>
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;
}
}
/// <summary>
/// Get managed string from xloc status response and free the C string
/// </summary>
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
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
using System.Runtime.InteropServices;
using RobotNet10.RobotApp.Navigation;
namespace RobotNet10.RobotApp.Xloc;
/// <summary>
/// C-compatible structs for xloc C API interop
/// Based on /home/robotics/sonvh/xloc_Linux_0.1.0/_CPack_Packages/Linux/DEB/xloc-0.1.0-Linux/usr/include/xloc/xloc_c_api.h
/// </summary>
/// <summary>
/// Unix timestamp with seconds and nanoseconds (xloc_unix_time_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_unix_time_t
{
public uint sec;
public uint nsec;
}
/// <summary>
/// Message header (xloc_header_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_header_t
{
public uint seq;
public xloc_unix_time_t stamp;
public IntPtr frame_id; // char* - must be allocated and freed
}
/// <summary>
/// Odometry message (xloc_odometry_t)
/// CRITICAL: Must match exact C API definition in xloc_c_api.h (lines 138-149)
/// Uses flattened arrays, NOT nested xloc_pose_t or xloc_twist_t structs
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_odometry_t
{
public xloc_header_t header;
public IntPtr child_frame_id; // char* - must be allocated and freed
// Pose with covariance (flattened, not using xloc_pose_t)
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public double[] pose_position; // [3] - x, y, z
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public double[] pose_orientation; // [4] - x, y, z, w (quaternion)
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)]
public double[] pose_covariance; // [36] - 6x6 matrix
// Twist with covariance (flattened, not using xloc_twist_t)
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public double[] twist_linear; // [3] - x, y, z
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public double[] twist_angular; // [3] - x, y, z
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)]
public double[] twist_covariance; // [36] - 6x6 matrix
}
/// <summary>
/// IMU message (xloc_imu_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_imu_t
{
public xloc_header_t header;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public double[] orientation; // [4] - x, y, z, w (quaternion)
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
public double[] orientation_covariance; // [9] - 3x3 matrix
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public double[] angular_velocity; // [3] - x, y, z
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
public double[] angular_velocity_covariance; // [9] - 3x3 matrix
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public double[] linear_acceleration; // [3] - x, y, z
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
public double[] linear_acceleration_covariance; // [9] - 3x3 matrix
}
/// <summary>
/// LaserScan message (xloc_laserscan_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_laserscan_t
{
public xloc_header_t header;
public float angle_min;
public float angle_max;
public float angle_increment;
public float time_increment;
public float scan_time;
public float range_min;
public float range_max;
public IntPtr ranges; // float* - pointer to array
public nuint ranges_length; // size_t - number of elements
public IntPtr intensities; // float* - pointer to array
public nuint intensities_length; // size_t - number of elements
}
/// <summary>
/// Status response from xloc (xloc_status_response_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_status_response_t
{
public byte code;
public IntPtr message; // char* - must be freed with xloc_free_cstring
}
/// <summary>
/// Pose (xloc_pose_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_pose_t
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public double[] position; // [3] - x, y, z
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public double[] orientation; // [4] - x, y, z, w (quaternion)
}
/// <summary>
/// Diagnostics information from xloc (xloc_diagnostics_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_diagnostics_t
{
public xloc_header_t header;
public byte xloc_state; // 0=MAPPING, 1=LOCALIZATION, 2=PROCESSING, 3=READY, 4=ERROR
public IntPtr current_active_map; // char* - must be freed
public double reliability; // 0.0 to 1.0
public double matching_score; // SLAM matching quality
}
/// <summary>
/// Occupancy grid map (xloc_occupancy_grid_t)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct xloc_occupancy_grid_t
{
public xloc_header_t header;
public float resolution; // meters per cell
public uint width; // cells
public uint height; // cells
public xloc_pose_t origin; // pose of cell (0,0) in map frame
public IntPtr data; // int8_t* - pointer to occupancy data (width*height bytes)
public nuint data_length; // size_t - number of bytes
}

View File

@@ -0,0 +1,225 @@
using System.IO.Compression;
using Microsoft.AspNetCore.Http;
namespace RobotNet10.RobotApp.Xloc;
public static class XlocMapFileEndpoints
{
public static bool TryValidateMapFolderName(string mapName, out string safeName)
{
safeName = "";
if (string.IsNullOrWhiteSpace(mapName))
return false;
var t = mapName.Trim();
if (t.Length > 200)
return false;
if (string.Equals(t, "tmp", StringComparison.OrdinalIgnoreCase))
return false;
if (t.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
return false;
if (t.Contains('/', StringComparison.Ordinal) || t.Contains('\\', StringComparison.Ordinal))
return false;
safeName = t;
return true;
}
public static IResult DeleteMapFolder(string mapName)
{
try
{
if (!TryValidateMapFolderName(mapName, out var safeName))
return Results.BadRequest(new { status = "error", message = "Invalid map name" });
var mapsDir = XlocPaths.GetMapsDirectory();
if (!Directory.Exists(mapsDir))
return Results.NotFound(new { status = "error", message = "Maps directory not found" });
var mapFolder = Path.Combine(mapsDir, safeName);
if (!Directory.Exists(mapFolder))
return Results.NotFound(new { status = "error", message = $"Map '{safeName}' not found" });
Directory.Delete(mapFolder, recursive: true);
return Results.Ok(new { status = "success", message = "Map deleted", map_name = safeName });
}
catch (Exception ex)
{
return Results.BadRequest(new { status = "error", message = ex.Message });
}
}
public static IResult DownloadMapAsZip(string mapName)
{
try
{
if (!TryValidateMapFolderName(mapName, out var safeName))
return Results.BadRequest(new { status = "error", message = "Invalid map name" });
var mapsDir = XlocPaths.GetMapsDirectory();
var mapFolder = Path.Combine(mapsDir, safeName);
if (!Directory.Exists(mapFolder))
return Results.NotFound(new { status = "error", message = $"Map '{safeName}' not found" });
using var ms = new MemoryStream();
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var filePath in Directory.EnumerateFiles(mapFolder, "*", SearchOption.AllDirectories))
{
var rel = Path.GetRelativePath(mapFolder, filePath);
var entryName = $"{safeName}/{rel.Replace('\\', '/')}";
var entry = archive.CreateEntry(entryName, CompressionLevel.Fastest);
using var entryStream = entry.Open();
using var fileStream = File.OpenRead(filePath);
fileStream.CopyTo(entryStream);
}
}
return Results.File(ms.ToArray(), "application/zip", $"{safeName}.zip");
}
catch (Exception ex)
{
return Results.BadRequest(new { status = "error", message = ex.Message });
}
}
public static async Task<IResult> ImportMapFromZipAsync(HttpRequest request)
{
if (!request.HasFormContentType)
return Results.BadRequest(new { status = "error", message = "Multipart form required" });
var form = await request.ReadFormAsync();
var file = form.Files.GetFile("file");
if (file == null || file.Length == 0)
return Results.BadRequest(new { status = "error", message = "Zip file (field: file) required" });
if (!file.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
return Results.BadRequest(new { status = "error", message = "File must be a .zip archive" });
var zipStem = Path.GetFileNameWithoutExtension(file.FileName);
if (string.IsNullOrWhiteSpace(zipStem) || zipStem is "." or "..")
return Results.BadRequest(new { status = "error", message = "Invalid zip file name" });
if (!TryValidateMapFolderName(zipStem, out var finalTargetName))
return Results.BadRequest(new
{
status = "error",
message = "Map name is taken from the zip file name. Rename the file (letters, numbers, dash, underscore; no path characters)."
});
var tempZip = Path.Combine(Path.GetTempPath(), $"xloc_import_{Guid.NewGuid():N}.zip");
var tempExtract = Path.Combine(Path.GetTempPath(), $"xloc_extract_{Guid.NewGuid():N}");
try
{
await using (var fs = File.Create(tempZip))
await file.CopyToAsync(fs);
Directory.CreateDirectory(tempExtract);
ZipFile.ExtractToDirectory(tempZip, tempExtract);
if (!TryResolveImportedMap(tempExtract, out var sourceDir))
return Results.BadRequest(new
{
status = "error",
message = "Could not find a valid map. Use a zip with one top-level folder containing a .yaml file, or with .yaml files at the archive root."
});
var mapsDir = XlocPaths.GetMapsDirectory();
Directory.CreateDirectory(mapsDir);
var destPath = Path.Combine(mapsDir, finalTargetName);
if (Directory.Exists(destPath))
return Results.Conflict(new { status = "error", message = $"A map named '{finalTargetName}' already exists. Rename the zip file or remove the existing map folder." });
if (string.Equals(sourceDir, tempExtract, StringComparison.Ordinal))
{
Directory.CreateDirectory(destPath);
CopyDirectoryRecursive(tempExtract, destPath);
}
else
{
Directory.Move(sourceDir, destPath);
}
return Results.Ok(new { status = "success", map_name = finalTargetName });
}
catch (Exception ex)
{
return Results.BadRequest(new { status = "error", message = ex.Message });
}
finally
{
TryDeleteFile(tempZip);
TryDeleteDirectory(tempExtract);
}
}
private static bool TryResolveImportedMap(string extractRoot, out string sourceDir)
{
sourceDir = "";
var rootFiles = Directory.GetFiles(extractRoot);
var rootDirs = Directory.GetDirectories(extractRoot)
.Where(d => !string.Equals(Path.GetFileName(d), "__MACOSX", StringComparison.OrdinalIgnoreCase))
.ToArray();
if (Directory.GetFiles(extractRoot, "*.yaml", SearchOption.TopDirectoryOnly).Length > 0)
{
sourceDir = extractRoot;
return true;
}
var dirsWithYaml = rootDirs
.Where(d => Directory.GetFiles(d, "*.yaml", SearchOption.AllDirectories).Length > 0)
.ToList();
if (dirsWithYaml.Count == 1 && rootFiles.Length == 0)
{
sourceDir = dirsWithYaml[0];
return true;
}
return false;
}
private static void CopyDirectoryRecursive(string sourceDir, string destDir)
{
foreach (var file in Directory.GetFiles(sourceDir))
{
var destFile = Path.Combine(destDir, Path.GetFileName(file));
File.Copy(file, destFile, overwrite: false);
}
foreach (var dir in Directory.GetDirectories(sourceDir))
{
var destSub = Path.Combine(destDir, Path.GetFileName(dir));
Directory.CreateDirectory(destSub);
CopyDirectoryRecursive(dir, destSub);
}
}
private static void TryDeleteFile(string path)
{
try
{
if (File.Exists(path))
File.Delete(path);
}
catch
{
// ignore
}
}
private static void TryDeleteDirectory(string path)
{
try
{
if (Directory.Exists(path))
Directory.Delete(path, recursive: true);
}
catch
{
// ignore
}
}
}

View File

@@ -0,0 +1,217 @@
using System.Runtime.InteropServices;
namespace RobotNet10.RobotApp.Xloc;
/// <summary>
/// P/Invoke declarations for xloc C API
/// </summary>
public static class XlocNativeInterface
{
// Library path - adjust/ if needed
private const string LibraryPath = "/usr/lib/libxloc.so";
#region Creation / Destruction
/// <summary>
/// Create an xloc instance
/// </summary>
/// <param name="tfBuffer">TF3 buffer core (pass IntPtr.Zero if not using TF)</param>
/// <returns>Handle to xloc instance</returns>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr xloc_create(IntPtr tfBuffer);
/// <summary>
/// Destroy an xloc instance
/// </summary>
/// <param name="handle">Handle to xloc instance</param>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_destroy(IntPtr handle);
#endregion
#region Sensor Data Dispatch
/// <summary>
/// Dispatch laser scan data to xloc
/// </summary>
/// <param name="handle">Handle to xloc instance</param>
/// <param name="sensorId">Sensor ID (null-terminated string)</param>
/// <param name="scan">Pointer to laser scan struct</param>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_dispatch_laserscan(
IntPtr handle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string sensorId,
ref xloc_laserscan_t scan);
/// <summary>
/// Dispatch IMU data to xloc
/// </summary>
/// <param name="handle">Handle to xloc instance</param>
/// <param name="sensorId">Sensor ID (null-terminated string)</param>
/// <param name="imu">Pointer to IMU struct</param>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_dispatch_imu(
IntPtr handle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string sensorId,
ref xloc_imu_t imu);
/// <summary>
/// Dispatch odometry data to xloc
/// </summary>
/// <param name="handle">Handle to xloc instance</param>
/// <param name="sensorId">Sensor ID (null-terminated string)</param>
/// <param name="odom">Pointer to odometry struct</param>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_dispatch_odometry(
IntPtr handle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string sensorId,
ref xloc_odometry_t odom);
#endregion
#region Localization Control
/// <summary>
/// Activate a map
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_activate_map(
IntPtr handle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string mapFileName);
/// <summary>
/// Switch to a different map
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_switch_map(
IntPtr handle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string mapFileName,
int useInitialPose,
ref xloc_pose_t initialPose);
/// <summary>
/// Start localization
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_start_localization(IntPtr handle);
/// <summary>
/// Stop localization
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_stop_localization(IntPtr handle);
/// <summary>
/// Start mapping
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_start_mapping(IntPtr handle);
/// <summary>
/// Stop mapping and optionally save the map
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_stop_mapping(
IntPtr handle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string mapFileName);
/// <summary>
/// Set initial pose
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_set_initial_pose(
IntPtr handle,
ref xloc_pose_t initialPose);
/// <summary>
/// Change map origin
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_change_map_origin(
IntPtr handle,
ref xloc_pose_t newMapOrigin);
/// <summary>
/// Start updating existing map
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_start_update_map(IntPtr handle);
/// <summary>
/// Stop updating map and optionally save
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_stop_update_map(
IntPtr handle,
int saveUpdatedMap);
/// <summary>
/// Reset SLAM error state
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern xloc_status_response_t xloc_reset_slam_error(IntPtr handle);
#endregion
#region Getters
/// <summary>
/// Get current pose estimate
/// </summary>
/// <returns>Pointer to pose (must be freed with xloc_free_pose)</returns>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr xloc_get_current_pose(IntPtr handle);
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr xloc_get_diagnostics(IntPtr handle);
/// <summary>
/// Get static grid map (from loaded map file)
/// </summary>
/// <param name="handle">Handle to xloc instance</param>
/// <param name="reloadFromFile">If non-zero, reload map from file</param>
/// <returns>Pointer to occupancy grid (must be freed with xloc_free_occupancy_grid)</returns>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr xloc_get_static_grid_map(IntPtr handle, int reloadFromFile);
/// <summary>
/// Get online grid map (from SLAM)
/// </summary>
/// <param name="handle">Handle to xloc instance</param>
/// <returns>Pointer to occupancy grid (must be freed with xloc_free_occupancy_grid)</returns>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr xloc_get_online_grid_map(IntPtr handle);
#endregion
#region Memory Management
/// <summary>
/// Free status response
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_free_status_response(ref xloc_status_response_t response);
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_free_diagnostics(IntPtr diagnostics);
/// <summary>
/// Free C string allocated by xloc
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_free_cstring(IntPtr str);
/// <summary>
/// Free pose allocated by xloc
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_free_pose(IntPtr pose);
/// <summary>
/// Free occupancy grid allocated by xloc
/// </summary>
[DllImport(LibraryPath, CallingConvention = CallingConvention.Cdecl)]
public static extern void xloc_free_occupancy_grid(IntPtr grid);
#endregion
}

View File

@@ -0,0 +1,65 @@
using System;
using System.IO;
namespace RobotNet10.RobotApp.Xloc;
public static class XlocPaths
{
private static string GetHomeDir()
{
// systemd/Docker services thường chạy với HOME=/root hoặc thiếu HOME
var home = Environment.GetEnvironmentVariable("HOME");
if (!string.IsNullOrWhiteSpace(home))
return home;
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
public static string GetXdgDataHome()
{
// XDG_DATA_HOME mặc định là $HOME/.local/share
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
if (!string.IsNullOrWhiteSpace(xdg))
return xdg;
return Path.Combine(GetHomeDir(), ".local", "share");
}
public static string GetMapsDirectory()
{
return Path.Combine(GetXdgDataHome(), "xloc", "resources", "maps");
}
/// <summary>
/// Resolve a map folder name (e.g. "map_20260307_103646") into a pbstream file path.
/// If <paramref name="mapFilePathOrName"/> is already an absolute existing file, returns it as-is.
/// </summary>
public static string? ResolvePbstreamPath(string mapFilePathOrName)
{
if (string.IsNullOrWhiteSpace(mapFilePathOrName))
return null;
// If caller already provided an absolute pbstream path, just validate it.
if (Path.IsPathRooted(mapFilePathOrName) && File.Exists(mapFilePathOrName))
return mapFilePathOrName;
// Otherwise treat it as a map folder name under the configured maps directory.
var mapsDir = GetMapsDirectory();
var mapName = Path.GetFileName(mapFilePathOrName.TrimEnd(Path.DirectorySeparatorChar, '/'));
if (string.IsNullOrWhiteSpace(mapName))
return null;
var mapFolder = Path.Combine(mapsDir, mapName);
if (!Directory.Exists(mapFolder))
return null;
// Prefer canonical name: map.pbstream, then fall back to any *.pbstream.
var canonical = Path.Combine(mapFolder, "map.pbstream");
if (File.Exists(canonical))
return canonical;
var pbstreams = Directory.GetFiles(mapFolder, "*.pbstream", SearchOption.TopDirectoryOnly);
return pbstreams.Length > 0 ? pbstreams[0] : null;
}
}