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;

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(&current_pose) {
    println!("Goal reached!");
}

// Check position and orientation separately
if goal.is_position_reached(&current_pose) {
    println!("Position reached, adjusting orientation...");
}
if goal.is_orientation_reached(&current_pose) {
    println!("Orientation reached");
}

Fields:

FieldTypeDescription
target_posePose2DTarget pose to reach
tolerance_positionf64Position tolerance (meters)
tolerance_anglef64Orientation tolerance (radians)
timeout_secondsf64Maximum time (0 = no limit)
priorityu8Goal priority (0 = highest)
goal_idu32Unique goal identifier
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
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(&current_pose)Check if position is within tolerance
is_orientation_reached(&current_pose)Check if orientation is within tolerance
is_reached(&current_pose)Check if both position and orientation are reached

GoalStatus

Goal execution status enumeration.

Note: a bare use horus::prelude::*; gives you the action GoalStatus (horus_core::actions::GoalStatus — variants Pending/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:

StatusValueDescription
Pending0Goal pending execution (default)
Active1Actively pursuing goal
Succeeded2Goal reached successfully
Aborted3Navigation aborted (error)
Cancelled4Cancelled by user
Preempted5Preempted by higher priority
TimedOut6Goal timed out

GoalResult

Goal status feedback with progress information. Implements PodMessage for zero-copy transfer.

Note: GoalResult is in the prelude, but the GoalStatus it takes is not — the prelude's GoalStatus is 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:

FieldTypeDescription
goal_idu32Goal identifier
statusu8Current status (set directly with e.g. GoalStatus::Active as u8)
distance_to_goalf64Distance remaining (meters)
eta_secondsf64Estimated time to arrive
progressf32Progress (0.0 to 1.0)
error_message[u8; 64]Error message if failed
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
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:

FieldTypeDescription
posePose2DWaypoint pose (x, y, theta)
velocityTwistDesired velocity at this point
time_from_startf64Time from path start (seconds)
curvaturef32Path curvature (1/radius)
stop_requiredu8Whether to stop at waypoint (0 = no, 1 = yes)

Methods:

MethodDescription
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

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(&current) {
    println!("Closest waypoint: {}", idx);
}

// Calculate progress along path
let progress = path.calculate_progress(&current);
println!("Path progress: {:.0}%", progress * 100.0);

Fields:

FieldTypeDescription
waypoints[Waypoint; 256]Array of waypoints
waypoint_countu16Number of valid waypoints
total_lengthf64Total path length (meters)
duration_secondsf64Estimated completion time
frame_id[u8; 32]Coordinate frame
algorithm[u8; 32]Planning algorithm used
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
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:

FieldTypeDescription
waypoint_data[f32; 768]Packed waypoint data (256 × 3 floats: x, y, theta)
goal_pose[f32; 3]Goal pose [x, y, theta]
waypoint_countu16Number of valid waypoints
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
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:

ValueMeaning
-1Unknown
0Free
1-49Probably free
50-99Probably occupied
100Occupied

Accessors: the two struct fields (descriptor and pool) are private, so every value below is reached through a method call, not field access.

AccessorReturnsDescription
resolution()f32Meters per cell
width()u32Map width in cells
height()u32Map height in cells
cell_count()usizewidth * height
origin_x() / origin_y() / origin_theta()f64Map origin pose, one component per call
set_origin(x, y, theta)&mut SelfMove the origin
cells() / cells_mut()&[i8] / &mut [i8]The occupancy cells
metadata()&[u8; 64]Map metadata
frame_id()&strCoordinate frame
timestamp_ns()u64Nanoseconds since epoch

Methods:

MethodDescription
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:

ValueMeaning
0Free space
1-253Increasing cost (near obstacles)
254Lethal (the default lethal_cost)
255Above lethal — treat as blocked

Accessors: descriptor and pool are the only fields and both are private, so each value below is a method call.

AccessorReturnsDescription
width() / height()u32Map dimensions in cells
resolution()f32Meters per cell
cell_count()usizewidth * height
inflation_radius()f32Inflation radius in meters (default 0.3)
cost_scaling_factor()f32Cost decay factor (default 10.0)
lethal_cost()u8Lethal 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()f64Map origin pose
frame_id()&strCoordinate frame
timestamp_ns()u64Nanoseconds since epoch

Methods:

MethodDescription
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:

FieldTypeDescription
position[f64; 2]Obstacle position [x, y]
velocity[f64; 2]Obstacle velocity [vx, vy]
radiusf32Obstacle radius (meters)
time_horizonf32Collision prediction horizon
obstacle_idu32Tracking 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:

FieldTypeDescription
obstacles[VelocityObstacle; 32]Obstacle array
countu8Number of valid obstacles
timestamp_nsu64Nanoseconds since epoch
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(&current_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(&current_pose);
                if let Some(path) = &self.current_path {
                    result.progress = path.calculate_progress(&current_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