Using Pre-Built Nodes

The HORUS Philosophy: Don't reinvent the wheel. Use comprehensive, battle-tested nodes from the registry, wire them together with the standard message types in the HORUS prelude, and 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() {
    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).done();

    scheduler.run().expect("Scheduler failed");
}

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


Discovering Pre-Built Nodes

From the Registry

Web Interface: Open horusrobotics.dev in your browser to browse the registry.

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 search sensor
horus search controller
horus search motor

From the Standard Prelude (Message Types)

The horus::prelude 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 the HORUS prelude. 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@2.1.0

# One package per invocation
horus install pid-controller
horus install kalman-filter
horus install sensor-drivers

Installing from crates.io

# Rust packages are auto-detected
horus install serde
horus install tokio@1.35.0

Installing from PyPI

# Python packages are auto-detected
horus install numpy
horus install opencv-python

Using Standard Library

The standard library is available automatically with horus run:

horus run main.rs
# the HORUS standard library is included by default

Or explicitly in horus.toml:

[dependencies]
horus = "0.2"

For cargo projects (not using horus run), put the same entry in Cargo.toml:

[dependencies]
horus = "0.2"

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 search keyboard
horus search 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).done();

    // 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).done();

    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).done();

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

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

    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).done();
    scheduler.add(ImuNode::new("/dev/i2c-1", "imu")?).order(2).done();

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

    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).done();
    scheduler.add(processor).order(3).done();
    scheduler.add(detector).order(3).done();

    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).done();

Parameter-Based Configuration

Configure nodes via the parameter system:

use horus::prelude::*;

// Set parameters via RuntimeParams
let params = RuntimeParams::new()?;
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).done();

Adjust at runtime: horus param set motor.max_speed 3.0

Reproducing the Setup on Another Robot

Project dependencies live in horus.toml -- add them with horus add, which writes to [dependencies], or horus add <pkg> --dev to write to [dev-dependencies] for something only your tests need. (horus install fetches a standalone package and does not touch the manifest, so it is not enough on its own to reproduce a setup.)

# Pin every dependency version into horus.lock
horus lock

# Commit horus.toml and horus.lock, then on the other robot:
#   git pull && horus build

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).done();       // Publishes "raw"
scheduler.add(filter).order(3).done();       // Subscribes "raw", publishes "filtered"
scheduler.add(controller).order(4).done();   // Subscribes "filtered", publishes "cmd"
scheduler.add(actuator).order(5).done();     // Subscribes "cmd"

Parallel Processing

Multiple nodes at same priority run concurrently:

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

Safety Layering

Critical nodes run first:

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

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

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

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

When to Build Custom Nodes

Use pre-built nodes when:

  • Functionality exists in the registry or the HORUS prelude
  • 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 search lidar
horus search camera
horus search 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).done();

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

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

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

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

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

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


Next Steps