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.

⚠️The registry commands on this page need a registry

There is no public HORUS package registry yet, so horus search and horus install <name> on this page have nothing to query. horus search returns local results only and tells you the registry was not consulted — an empty result here means "not searched", not "nothing exists".

The patterns on this page are still the intended way to build with HORUS. Until the registry is up, get the same nodes through path or git dependencies in horus.toml (see Configuration), or point HORUS_REGISTRY_URL at a self-hosted registry.

Progress: softmata/horus#173.

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 = "*"
⚠️`horus` is not on crates.io

Do not write horus = "0.4" in a Cargo.toml. Cargo would resolve it against crates.io, where the name is owned by an unrelated project, and the first use horus::prelude::*; fails to compile against it. HORUS's own crates carry git and path dependencies that cargo package rejects, so none of them are published there and none will resolve.

horus run and horus build generate a Cargo.toml that points at the source tree the installer cached — ~/.horus/cache/horus@<version> — as a path dependency. That is why the installer keeps that tree, and why a version number here has nothing to resolve against.

For a plain cargo project (not using horus run), depend on that same tree by path:

[dependencies]
horus = { path = "/home/you/.horus/cache/horus@0.4.0/horus" }

horus --version prints the version to substitute. Prefer horus run, which writes this for you and keeps it in step with the installed CLI.


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.)

# Record the resolved dependency versions into horus.lock
horus lock

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

The installers read horus.lock's [[package]] pins before resolving, so the other robot installs the versions this one recorded rather than whatever latest gives it that day. A horus build there may add a pin for a package that has none; it will not move one that is already committed.

Run horus lock --check in CI: it compares the pins against what is actually installed and exits non-zero on drift, naming the package.

Pin the framework itself with HORUS_VERSION when you install on the robot — horus.lock covers the project's dependencies, not the HORUS release under them. Toolchain pins ([toolchain]) are recorded and reported but not enforced. See Environment Management.


Composing Complex Systems

Pipeline Pattern

Chain nodes together via topics:

Loading diagram...
Prebuilt nodes compose over topics — each stage only needs the message type, not the node that produced it
// 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