Navigation Messages
HORUS provides message types for autonomous navigation, path planning, mapping, and localization systems.
These types live in horus_robotics::messages::navigation and are re-exported through
horus::prelude, so use horus::prelude::*; is all you need for: NavGoal, GoalResult,
Waypoint, NavPath, PathPlan, OccupancyGrid, CostMap, VelocityObstacle,
VelocityObstacles.
One exception: the name GoalStatus is already taken. horus::prelude explicitly
re-exports horus_core::actions::GoalStatus, and an explicit import shadows the glob — so
GoalStatus from the prelude is the action enum, not the navigation one. The prelude also
re-exports the navigation module itself, so reach the navigation enum through that:
use horus::prelude::navigation::GoalStatus;
NavGoal
Navigation goal specification with tolerance and timeout. Implements PodMessage for zero-copy transfer.
use horus::prelude::*;
// Create navigation goal
let target = Pose2D::new(5.0, 3.0, 1.57); // x, y, theta
let goal = NavGoal::new(target, 0.1, 0.05); // 10cm position, 0.05rad angle tolerance
// With timeout and priority
let goal = NavGoal::new(target, 0.1, 0.05)
.with_timeout(30.0) // 30 second timeout
.with_priority(0); // Highest priority
// Check if goal reached
let current_pose = Pose2D::new(5.05, 3.02, 1.55);
if goal.is_reached(¤t_pose) {
println!("Goal reached!");
}
// Check position and orientation separately
if goal.is_position_reached(¤t_pose) {
println!("Position reached, adjusting orientation...");
}
if goal.is_orientation_reached(¤t_pose) {
println!("Orientation reached");
}
Fields:
| Field | Type | Description |
|---|---|---|
target_pose | Pose2D | Target pose to reach |
tolerance_position | f64 | Position tolerance (meters) |
tolerance_angle | f64 | Orientation tolerance (radians) |
timeout_seconds | f64 | Maximum time (0 = no limit) |
priority | u8 | Goal priority (0 = highest) |
goal_id | u32 | Unique goal identifier |
timestamp_ns | u64 | Nanoseconds since epoch |
Methods:
| Method | Description |
|---|---|
new(target_pose, tolerance_position, tolerance_angle) | Create a new navigation goal |
with_timeout(seconds) | Set timeout (builder pattern) |
with_priority(priority) | Set priority (builder pattern) |
is_position_reached(¤t_pose) | Check if position is within tolerance |
is_orientation_reached(¤t_pose) | Check if orientation is within tolerance |
is_reached(¤t_pose) | Check if both position and orientation are reached |
GoalStatus
Goal execution status enumeration.
Note: a bare
use horus::prelude::*;gives you the actionGoalStatus(horus_core::actions::GoalStatus— variantsPending/Active/Succeeded/Aborted/Canceled/Preempted/Rejected), whose explicit re-export shadows the navigation enum. Import the navigation one through its module:use horus::prelude::navigation::GoalStatus;
use horus::prelude::navigation::GoalStatus;
let status = GoalStatus::Active;
match status {
GoalStatus::Pending => println!("Waiting to start"),
GoalStatus::Active => println!("Moving to goal"),
GoalStatus::Succeeded => println!("Goal reached!"),
GoalStatus::Aborted => println!("Navigation failed"),
GoalStatus::Cancelled => println!("Goal cancelled by user"),
GoalStatus::Preempted => println!("Higher priority goal received"),
GoalStatus::TimedOut => println!("Goal timed out"),
}
Status Values:
| Status | Value | Description |
|---|---|---|
Pending | 0 | Goal pending execution (default) |
Active | 1 | Actively pursuing goal |
Succeeded | 2 | Goal reached successfully |
Aborted | 3 | Navigation aborted (error) |
Cancelled | 4 | Cancelled by user |
Preempted | 5 | Preempted by higher priority |
TimedOut | 6 | Goal timed out |
GoalResult
Goal status feedback with progress information. Implements PodMessage for zero-copy transfer.
Note:
GoalResultis in the prelude, but theGoalStatusit takes is not — the prelude'sGoalStatusis the action enum. Import the navigation one through its module:use horus::prelude::navigation::GoalStatus;
use horus::prelude::*;
use horus::prelude::navigation::GoalStatus;
// Create success result
let result = GoalResult::new(42, GoalStatus::Succeeded);
// Create failure result with error
let error_result = GoalResult::new(42, GoalStatus::Aborted)
.with_error("Obstacle blocking path");
// Update progress
let mut in_progress = GoalResult::new(42, GoalStatus::Active);
in_progress.distance_to_goal = 2.5; // 2.5m remaining
in_progress.eta_seconds = 5.0; // 5s estimated
in_progress.progress = 0.75; // 75% complete
println!("Goal {}: status={}, {:.1}m to go, ETA {:.1}s",
in_progress.goal_id, in_progress.status,
in_progress.distance_to_goal, in_progress.eta_seconds);
Fields:
| Field | Type | Description |
|---|---|---|
goal_id | u32 | Goal identifier |
status | u8 | Current status (set directly with e.g. GoalStatus::Active as u8) |
distance_to_goal | f64 | Distance remaining (meters) |
eta_seconds | f64 | Estimated time to arrive |
progress | f32 | Progress (0.0 to 1.0) |
error_message | [u8; 64] | Error message if failed |
timestamp_ns | u64 | Nanoseconds since epoch |
Methods:
| Method | Description |
|---|---|
new(goal_id, status) | Create a new result (takes the navigation GoalStatus, stores as u8) |
with_error(message) | Set error message string (builder pattern) |
Waypoint
Single waypoint in a navigation path. Implements PodMessage for zero-copy transfer.
use horus::prelude::*;
// Simple waypoint
let wp = Waypoint::new(Pose2D::new(1.0, 2.0, 0.0));
// Waypoint with velocity profile
let wp = Waypoint::new(Pose2D::new(1.0, 2.0, 0.0))
.with_velocity(Twist::new_2d(0.5, 0.0)); // 0.5 m/s forward
// Waypoint requiring stop (e.g., for pickup)
let stop_wp = Waypoint::new(Pose2D::new(3.0, 4.0, 1.57))
.with_stop();
// Access properties
println!("Position: ({:.1}, {:.1})", wp.pose.x, wp.pose.y);
println!("Curvature: {:.3}", wp.curvature);
println!("Stop required: {}", wp.stop_required); // 0 = no, 1 = yes
Fields:
| Field | Type | Description |
|---|---|---|
pose | Pose2D | Waypoint pose (x, y, theta) |
velocity | Twist | Desired velocity at this point |
time_from_start | f64 | Time from path start (seconds) |
curvature | f32 | Path curvature (1/radius) |
stop_required | u8 | Whether to stop at waypoint (0 = no, 1 = yes) |
Methods:
| Method | Description |
|---|---|
new(pose) | Create a new waypoint with default velocity |
with_velocity(twist) | Set desired velocity (builder pattern) |
with_stop() | Mark as requiring stop, sets velocity to zero |
NavPath
Navigation path with up to 256 waypoints. Implements PodMessage for zero-copy transfer.
use horus::prelude::*;
// Create empty path
let mut path = NavPath::new();
// Returns `Result<(), &'static str>` ("Maximum 256 waypoints supported"), so
// `?` needs a conversion inside a function returning `Result<T>`:
path.add_waypoint(Waypoint::new(Pose2D::new(0.0, 0.0, 0.0))).map_err(|e| Error::node("nav", e))?;
path.add_waypoint(Waypoint::new(Pose2D::new(1.0, 0.0, 0.0))).map_err(|e| Error::node("nav", e))?;
path.add_waypoint(Waypoint::new(Pose2D::new(2.0, 1.0, 0.785))).map_err(|e| Error::node("nav", e))?;
// Get path info
println!("Waypoints: {}", path.waypoint_count);
println!("Total length: {:.2}m", path.total_length);
// Get valid waypoints slice
let waypoints = path.waypoints();
// Find closest waypoint to current position
let current = Pose2D::new(1.2, 0.3, 0.0);
if let Some(idx) = path.closest_waypoint_index(¤t) {
println!("Closest waypoint: {}", idx);
}
// Calculate progress along path
let progress = path.calculate_progress(¤t);
println!("Path progress: {:.0}%", progress * 100.0);
Fields:
| Field | Type | Description |
|---|---|---|
waypoints | [Waypoint; 256] | Array of waypoints |
waypoint_count | u16 | Number of valid waypoints |
total_length | f64 | Total path length (meters) |
duration_seconds | f64 | Estimated completion time |
frame_id | [u8; 32] | Coordinate frame |
algorithm | [u8; 32] | Planning algorithm used |
timestamp_ns | u64 | Nanoseconds since epoch |
Methods:
| Method | Description |
|---|---|
new() | Create a new empty path |
add_waypoint(waypoint) | Add a waypoint (returns Result, max 256) |
waypoints() | Get slice of valid waypoints |
closest_waypoint_index(&pose) | Find index of closest waypoint |
calculate_progress(&pose) | Calculate progress along path (0.0 to 1.0) |
PathPlan
Fixed-size path plan for zero-copy IPC transfer. Stores up to 256 waypoints as packed [x, y, theta] f32 values. Implements PodMessage for zero-copy transfer.
use horus::prelude::*;
// Create path plan from waypoints
let waypoints = &[
[0.0f32, 0.0, 0.0], // [x, y, theta]
[1.0, 0.0, 0.0],
[2.0, 0.5, 0.5],
[3.0, 1.0, 0.785],
];
let goal = [3.0f32, 1.0, 0.785];
let plan = PathPlan::from_waypoints(waypoints, goal);
// Or build incrementally
let mut plan = PathPlan::new();
plan.add_waypoint(0.0, 0.0, 0.0);
plan.add_waypoint(1.0, 0.5, 0.2);
plan.goal_pose = [1.0, 0.5, 0.2];
println!("Path has {} waypoints", plan.waypoint_count);
println!("Empty: {}", plan.is_empty());
// Access individual waypoints
if let Some(wp) = plan.waypoint(0) {
println!("First waypoint: x={}, y={}, theta={}", wp[0], wp[1], wp[2]);
}
Fields:
| Field | Type | Description |
|---|---|---|
waypoint_data | [f32; 768] | Packed waypoint data (256 × 3 floats: x, y, theta) |
goal_pose | [f32; 3] | Goal pose [x, y, theta] |
waypoint_count | u16 | Number of valid waypoints |
timestamp_ns | u64 | Nanoseconds since epoch |
Methods:
| Method | Description |
|---|---|
new() | Create a new empty path plan |
from_waypoints(waypoints, goal) | Create from a slice of [f32; 3] waypoints |
add_waypoint(x, y, theta) | Add a waypoint (returns bool, max 256) |
waypoint(index) | Get waypoint at index (u16) as Option<[f32; 3]> |
is_empty() | Check if path has no waypoints |
OccupancyGrid
2D occupancy grid map for navigation. The i8 cells live in a shared-memory tensor
allocated from the global pool, not in a Vec — what travels on a topic is a
fixed-size 320-byte Pod descriptor (dimensions, resolution, origin, frame, timestamp)
holding a handle to that tensor, copied byte-for-byte into the ring with no
serialization step.
Two different types are called OccupancyGrid. horus::prelude::* brings in the
navigation message (horus_robotics::messages::navigation::OccupancyGrid, a
plain Vec<i8> you put on a topic). The tensor-backed map documented here lives in
horus::memory and must be imported explicitly — the prelude re-exports Image,
DepthImage and PointCloud from horus::memory, but not this one.
use horus::prelude::*;
// Explicit import: shadows the navigation message of the same name from the prelude.
use horus::memory::OccupancyGrid;
// Create 10m x 10m map at 5cm resolution
let mut grid = OccupancyGrid::new(
200, // width (200 * 0.05 = 10m)
200, // height
0.05, // resolution (5cm per cell)
)?;
// The origin defaults to (0, 0, 0); move it if the map is not world-aligned
grid.set_origin(0.0, 0.0, 0.0);
// Set occupancy (-1=unknown, 0=free, 100=occupied)
grid.set_cell(100, 100, 0); // Free cell
grid.set_cell(150, 150, 100); // Occupied cell (obstacle)
// World to grid coordinate conversion
if let Some((gx, gy)) = grid.world_to_grid(5.0, 5.0) {
println!("World (5.0, 5.0) -> Grid ({}, {})", gx, gy);
}
// Grid to world coordinate conversion
if let Some((x, y)) = grid.grid_to_world(100, 100) {
println!("Grid (100, 100) -> World ({:.2}, {:.2})", x, y);
}
// Check cell status
let test_x = 7.5;
let test_y = 7.5;
if grid.is_free(test_x, test_y) {
println!("({}, {}) is free", test_x, test_y);
} else if grid.is_occupied(test_x, test_y) {
println!("({}, {}) is occupied", test_x, test_y);
}
// Get occupancy value
if let Some((gx, gy)) = grid.world_to_grid(test_x, test_y) {
if let Some(value) = grid.get_cell(gx, gy) {
println!("Occupancy: {}", value);
}
}
Occupancy Values:
| Value | Meaning |
|---|---|
-1 | Unknown |
0 | Free |
1-49 | Probably free |
50-99 | Probably occupied |
100 | Occupied |
Accessors: the two struct fields (descriptor and pool) are private, so every
value below is reached through a method call, not field access.
| Accessor | Returns | Description |
|---|---|---|
resolution() | f32 | Meters per cell |
width() | u32 | Map width in cells |
height() | u32 | Map height in cells |
cell_count() | usize | width * height |
origin_x() / origin_y() / origin_theta() | f64 | Map origin pose, one component per call |
set_origin(x, y, theta) | &mut Self | Move the origin |
cells() / cells_mut() | &[i8] / &mut [i8] | The occupancy cells |
metadata() | &[u8; 64] | Map metadata |
frame_id() | &str | Coordinate frame |
timestamp_ns() | u64 | Nanoseconds since epoch |
Methods:
| Method | Description |
|---|---|
new(width, height, resolution) | Create a new grid, initialized to unknown (returns HorusResult<Self>) |
world_to_grid(x, y) | Convert world coordinates to grid indices (Option<(u32, u32)>) |
grid_to_world(grid_x, grid_y) | Convert grid indices to world coordinates, cell center (Option<(f64, f64)>) |
get_cell(grid_x, grid_y) | Get occupancy value at grid coordinates (Option<i8>) |
set_cell(grid_x, grid_y, value) | Set occupancy value, clamped to -1..100 (returns bool) |
is_free(x, y) | Check if world point is free (occupancy 0-49) |
is_occupied(x, y) | Check if world point is occupied (occupancy >= 50) |
is_unknown(grid_x, grid_y) | Check if a grid cell is still unknown |
row(y) | One row of cells (Option<&[i8]>) |
fill(value) / clear() / reset() | Bulk cell writes |
CostMap
Navigation cost map with obstacle inflation. A CostMap owns both layers itself
— an occupancy layer (i8) and a cost layer (u8) — each in its own shared-memory
tensor. It does not wrap a separate OccupancyGrid.
use horus::prelude::*;
// Explicit import: shadows the navigation message of the same name from the prelude.
use horus::memory::CostMap;
// Create the costmap directly: it allocates both layers
let mut costmap = CostMap::new(
200, // width
200, // height
0.05, // resolution (5cm per cell)
0.55, // inflation radius (55cm)
)?;
// Mark an obstacle on the occupancy layer, then project it onto the cost layer
costmap.set_occupancy(150, 150, 100);
costmap.apply_from_occupancy();
// Cost lookups are by GRID coordinates, so convert world coordinates first
if let Some((gx, gy)) = costmap.world_to_grid(5.0, 5.0) {
if let Some(cost) = costmap.get_cost(gx, gy) {
if cost >= costmap.lethal_cost() {
println!("Position is in obstacle!");
} else {
println!("Cost: {}", cost);
}
}
}
// Dimensions live on the costmap itself
println!("Map size: {}x{}", costmap.width(), costmap.height());
Cost Values:
| Value | Meaning |
|---|---|
0 | Free space |
1-253 | Increasing cost (near obstacles) |
254 | Lethal (the default lethal_cost) |
255 | Above lethal — treat as blocked |
Accessors: descriptor and pool are the only fields and both are private, so
each value below is a method call.
| Accessor | Returns | Description |
|---|---|---|
width() / height() | u32 | Map dimensions in cells |
resolution() | f32 | Meters per cell |
cell_count() | usize | width * height |
inflation_radius() | f32 | Inflation radius in meters (default 0.3) |
cost_scaling_factor() | f32 | Cost decay factor (default 10.0) |
lethal_cost() | u8 | Lethal obstacle threshold (default 254) |
occupancy_cells() / occupancy_cells_mut() | &[i8] / &mut [i8] | The occupancy layer |
cost_cells() / cost_cells_mut() | &[u8] / &mut [u8] | The cost layer |
origin_x() / origin_y() / origin_theta() | f64 | Map origin pose |
frame_id() | &str | Coordinate frame |
timestamp_ns() | u64 | Nanoseconds since epoch |
Methods:
| Method | Description |
|---|---|
new(width, height, resolution, inflation_radius) | Create a costmap, allocating both layers (returns HorusResult<Self>) |
get_cost(grid_x, grid_y) / set_cost(grid_x, grid_y, value) | Read/write the cost layer at grid coordinates |
get_occupancy(grid_x, grid_y) / set_occupancy(grid_x, grid_y, value) | Read/write the occupancy layer |
apply_from_occupancy() | Copy the occupancy layer onto the cost layer (occupancy >= 50 becomes lethal) |
is_lethal(grid_x, grid_y) / is_traversable(grid_x, grid_y) | Threshold tests against lethal_cost() |
world_to_grid(x, y) / grid_to_world(grid_x, grid_y) | Coordinate conversion |
clear_costs() / reset_occupancy() | Bulk resets of one layer |
VelocityObstacle
Dynamic obstacle for velocity-based avoidance. Implements PodMessage for zero-copy transfer.
use horus::prelude::*;
let obstacle = VelocityObstacle {
position: [3.0, 2.0], // [x, y]
velocity: [0.5, 0.0], // Moving at 0.5 m/s in x
radius: 0.3, // 30cm radius
time_horizon: 5.0, // 5 second prediction
obstacle_id: 1,
};
println!("Obstacle {} at ({:.1}, {:.1}) moving at ({:.1}, {:.1})",
obstacle.obstacle_id,
obstacle.position[0], obstacle.position[1],
obstacle.velocity[0], obstacle.velocity[1]);
Fields:
| Field | Type | Description |
|---|---|---|
position | [f64; 2] | Obstacle position [x, y] |
velocity | [f64; 2] | Obstacle velocity [vx, vy] |
radius | f32 | Obstacle radius (meters) |
time_horizon | f32 | Collision prediction horizon |
obstacle_id | u32 | Tracking ID |
VelocityObstacles
Array of velocity obstacles (max 32). Implements PodMessage for zero-copy transfer.
use horus::prelude::*;
let mut obstacles = VelocityObstacles::default();
obstacles.obstacles[0] = VelocityObstacle {
position: [2.0, 1.0],
velocity: [0.3, 0.1],
radius: 0.25,
time_horizon: 3.0,
obstacle_id: 1,
};
obstacles.count = 1;
println!("Tracking {} dynamic obstacles", obstacles.count);
Fields:
| Field | Type | Description |
|---|---|---|
obstacles | [VelocityObstacle; 32] | Obstacle array |
count | u8 | Number of valid obstacles |
timestamp_ns | u64 | Nanoseconds since epoch |
Navigation Node Example
use horus::prelude::*;
use horus::prelude::navigation::GoalStatus;
struct NavigationNode {
goal_sub: Topic<NavGoal>,
odom_sub: Topic<Odometry>,
map_sub: Topic<OccupancyGrid>,
path_pub: Topic<NavPath>,
result_pub: Topic<GoalResult>,
current_goal: Option<NavGoal>,
current_path: Option<NavPath>,
}
impl Node for NavigationNode {
fn name(&self) -> &str { "Navigation" }
fn tick(&mut self) {
// Check for new goals
if let Some(goal) = self.goal_sub.recv() {
self.current_goal = Some(goal);
// Plan path to goal
if let Some(map) = self.map_sub.recv() {
let path = self.plan_path(&goal, &map);
self.path_pub.send(path);
self.current_path = Some(path);
}
}
// Check goal progress
if let (Some(goal), Some(odom)) = (&self.current_goal, self.odom_sub.recv()) {
let current_pose = odom.pose; // Odometry.pose is Pose2D
if goal.is_reached(¤t_pose) {
let result = GoalResult::new(goal.goal_id, GoalStatus::Succeeded);
self.result_pub.send(result);
self.current_goal = None;
} else {
let mut result = GoalResult::new(goal.goal_id, GoalStatus::Active);
result.distance_to_goal = goal.target_pose.distance_to(¤t_pose);
if let Some(path) = &self.current_path {
result.progress = path.calculate_progress(¤t_pose);
}
self.result_pub.send(result);
}
}
}
}
impl NavigationNode {
fn plan_path(&self, _goal: &NavGoal, _map: &OccupancyGrid) -> NavPath {
// Path planning implementation (A*, RRT*, etc.)
NavPath::new()
}
}
See Also
- Geometry Messages - Pose2D, Twist, TransformStamped
- Sensor Messages - Odometry, IMU, LaserScan