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:
// 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:
- Starting with a similar pre-built node
- Forking and modifying it
- 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
- Package Management - Discover and manage packages
- node! Macro - When you need custom functionality
- Examples - See complete working systems