Using Pre-Built Nodes

The HORUS Philosophy: Don't reinvent the wheel. Use comprehensive, battle-tested nodes from the registry and horus_library, then configure them to work together.

Why Use Pre-Built Nodes?

Advantages of pre-built nodes:

  • Production-ready and tested
  • Configure instead of coding
  • Focus on application logic, not infrastructure
  • Nodes use standard HORUS interfaces for interoperability

Quick Example

Instead of writing a PID controller from scratch, just install and configure:

# Install from registry
horus install pid-controller
use pid_controller::PIDNode;
use horus::prelude::*;

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();

    // Configure the pre-built node
    let pid = PIDNode::new(1.0, 0.1, 0.01);  // kp, ki, kd
    scheduler.add(pid).order(5).build()?;

    scheduler.run()?;
    Ok(())
}

That's it! Production-ready PID control in 3 lines.


Discovering Pre-Built Nodes

From the Registry

Web Interface:

# Visit the registry in your browser
https://registry.horusrobotics.dev

Browse by category:

  • Control - PID controllers, motion planners
  • Perception - Camera, LIDAR, sensor fusion
  • Drivers - Motor controllers, sensor interfaces
  • Safety - Emergency stop, watchdogs
  • Utilities - Loggers, data recorders

CLI Search:

# Search for specific functionality
horus list sensor
horus list controller
horus list motor

From Standard Library

The horus_library crate includes standard message types used across nodes:

use horus::prelude::*;

// Motion messages
CmdVel, Twist, Pose2D, Odometry

// Sensor messages
LaserScan, Imu, BatteryState, PointCloud

// Input messages
KeyboardInput, JoystickInput

// And many more...

Note: Hardware-interfacing nodes (sensor drivers, motor controllers, etc.) are available as registry packages or Python nodes -- they are not built into horus_library. Search the registry for ready-made nodes.


Installation Patterns

Installing from HORUS Registry

# Latest version
horus install motion-planner

# Specific version
horus install sensor-fusion -v 2.1.0

# Multiple packages
horus install pid-controller motion-planner sensor-drivers

Adding crates.io Dependencies

# Add Rust project dependencies (writes to horus.toml)
horus add serde
horus add tokio@1.35.0

Adding PyPI Dependencies

# Add Python project dependencies (writes to horus.toml)
horus add numpy
horus add opencv-python

Using Standard Library

The standard library is available automatically with horus run:

horus run main.rs
# horus_library is included by default

Or explicitly in your Cargo.toml:

[dependencies]
horus = { path = "..." }
horus_library = { path = "..." }

The Idiomatic Pattern

1. Discover What You Need

Example Goal: Build a mobile robot with keyboard control

Required Nodes:

  • Input: Keyboard control
  • Control: Velocity command processing
  • Output: Motor driver

2. Search and Install

# Check what's available
horus list keyboard
horus list motor

# Install what you need
horus install keyboard-input
horus install differential-drive

3. Configure and Compose

use keyboard_input::KeyboardNode;
use differential_drive::DiffDriveNode;
use horus::prelude::*;

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();

    // Keyboard input node (order 0 - runs first)
    let keyboard = KeyboardNode::new("keyboard.input")?;
    scheduler.add(keyboard).order(0).build()?;

    // Differential drive controller (order 5)
    let drive = DiffDriveNode::new(
        "keyboard.input",   // Input topic
        "motor.left",       // Left motor output
        "motor.right",      // Right motor output
        0.5                 // Wheel separation (meters)
    )?;
    scheduler.add(drive).order(5).build()?;

    scheduler.run()?;
    Ok(())
}

That's it! A functional robot in ~20 lines, no custom nodes needed.


Common Workflows

Mobile Robot Base

# Install components
horus install keyboard-input
horus install differential-drive
horus install emergency-stop
use keyboard_input::KeyboardNode;
use differential_drive::DiffDriveNode;
use emergency_stop::EStopNode;
use horus::prelude::*;

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();

    // Input
    scheduler.add(KeyboardNode::new("keyboard")?).order(0).build()?;

    // Safety (runs first!)
    scheduler.add(EStopNode::new("estop", "cmd_vel")?).order(0).build()?;

    // Drive control
    scheduler.add(DiffDriveNode::new("cmd_vel", "motor.left", "motor.right", 0.5)?)
        .order(1).build()?;

    scheduler.run()?;
    Ok(())
}

Sensor Fusion System

horus install lidar-driver
horus install imu-driver
horus install kalman-filter
use lidar_driver::LidarNode;
use imu_driver::ImuNode;
use kalman_filter::EKFNode;
use horus::prelude::*;

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();

    // Sensors (order 2)
    scheduler.add(LidarNode::new("/dev/ttyUSB0", "scan")?).order(2).build()?;
    scheduler.add(ImuNode::new("/dev/i2c-1", "imu")?).order(2).build()?;

    // Fusion (order 3 - runs after sensors)
    scheduler.add(EKFNode::new("scan", "imu", "pose")?).order(3).build()?;

    scheduler.run()?;
    Ok(())
}

Vision Processing Pipeline

# Install vision packages from registry
horus install camera-driver
horus install image-processor
horus install object-detector
use camera_driver::CameraNode;
use image_processor::ImageProcessorNode;
use object_detector::ObjectDetectorNode;
use horus::prelude::*;

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();

    // Using registry packages
    let camera = CameraNode::new("/dev/video0", "camera.raw", 30)?;
    let processor = ImageProcessorNode::new("camera.raw", "camera.processed")?;
    let detector = ObjectDetectorNode::new("camera.processed", "objects")?;

    scheduler.add(camera).order(2).build()?;
    scheduler.add(processor).order(3).build()?;
    scheduler.add(detector).order(3).build()?;

    scheduler.run()?;
    Ok(())
}

Configuration Best Practices

Use Builder Patterns

Many registry packages support fluent configuration:

// Example: camera-driver package from registry
let camera = CameraNode::new("/dev/video0")?
    .with_resolution(1920, 1080)
    .with_fps(60)
    .with_format(ImageFormat::RGB8);

scheduler.add(camera).order(2).build()?;

Parameter-Based Configuration

Configure nodes via the parameter system:

use horus::prelude::*;

// Set parameters via RuntimeParams
let params = RuntimeParams::init()?;
params.set("motor.max_speed", 2.0)?;
params.set("motor.acceleration", 0.5)?;

// Node reads from parameters
let motor = MotorNode::from_params()?;
scheduler.add(motor).order(1).build()?;

Adjust at runtime via monitor!

Reproducible Setup

Commit horus.lock to git to pin all dependency versions. On another machine, horus build will install the exact same versions.


Composing Complex Systems

Pipeline Pattern

Chain nodes together via topics:

Loading diagram...
// Each node subscribes to previous, publishes to next
scheduler.add(sensor).order(2).build()?;       // Publishes "raw"
scheduler.add(filter).order(3).build()?;       // Subscribes "raw", publishes "filtered"
scheduler.add(controller).order(4).build()?;   // Subscribes "filtered", publishes "cmd"
scheduler.add(actuator).order(5).build()?;     // Subscribes "cmd"

Parallel Processing

Multiple nodes at same priority run concurrently:

// All run in parallel (order 2)
scheduler.add(lidar).order(2).build()?;
scheduler.add(camera).order(2).build()?;
scheduler.add(imu).order(2).build()?;

Safety Layering

Critical nodes run first:

// Order 0 - Safety checks (runs first)
scheduler.add(watchdog).order(0).build()?;
scheduler.add(estop).order(0).build()?;

// Order 1 - Control
scheduler.add(controller).order(1).build()?;

// Order 2 - Sensors
scheduler.add(lidar).order(2).build()?;

// Order 4 - Logging (runs last)
scheduler.add(logger).order(4).build()?;

When to Build Custom Nodes

Use pre-built nodes when:

  • Functionality exists in the registry or horus_library
  • Node can be configured to your needs
  • Performance is acceptable

Build custom nodes when:

  • No existing node matches your hardware
  • Unique algorithm or business logic
  • Extreme performance requirements

Pro tip: Even then, consider:

  1. Starting with a similar pre-built node
  2. Forking and modifying it
  3. Publishing your improved version back to the registry

Finding the Right Node

By Use Case

I need to...

  • Control a motor motor-driver, differential-drive, servo-controller
  • Read a sensor lidar-driver, camera-node, imu-driver
  • Process data kalman-filter, pid-controller, image-processor
  • Handle safety emergency-stop, safety-monitor, watchdog
  • Log data data-logger, rosbag-writer, csv-logger

By Hardware

# Search by device type
horus list lidar
horus list camera
horus list imu

By Category

Browse registry by category:

  • control - Motion control, PID, path following
  • perception - Sensors, computer vision, SLAM
  • planning - Path planning, motion planning
  • drivers - Hardware interfaces
  • safety - Safety systems, fault tolerance
  • utils - Logging, visualization, debugging

Package Quality Indicators

When choosing packages, look for:

High Download Count

Downloads: 5,234 (last 30 days)

Recent Updates

Last updated: 2025-09-28

Good Documentation

Documentation: 98% coverage

Active Maintenance

Issues: 2 open, 45 closed (96% resolution rate)

Complete Example: Autonomous Robot

Goal: Build an autonomous mobile robot that avoids obstacles

1. Install Components:

horus install lidar-driver
horus install obstacle-detector
horus install path-planner
horus install differential-drive
horus install emergency-stop

2. Compose System:

use lidar_driver::LidarNode;
use obstacle_detector::ObstacleDetectorNode;
use path_planner::LocalPlannerNode;
use differential_drive::DiffDriveNode;
use emergency_stop::EStopNode;
use horus::prelude::*;

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();

    // Safety (order 0 - runs first)
    scheduler.add(EStopNode::new("estop", "cmd_vel")?).order(0).build()?;

    // Sensors (order 1)
    scheduler.add(LidarNode::new("/dev/ttyUSB0", "scan")?).order(1).build()?;

    // Perception (order 2)
    scheduler.add(ObstacleDetectorNode::new("scan", "obstacles")?).order(2).build()?;

    // Planning (order 3)
    scheduler.add(LocalPlannerNode::new("obstacles", "cmd_vel")?).order(3).build()?;

    // Control (order 4)
    scheduler.add(DiffDriveNode::new("cmd_vel", "motor.left", "motor.right", 0.5)?).order(4).build()?;

    scheduler.run()?;
    Ok(())
}

That's a full autonomous robot in ~40 lines of configuration!


Next Steps