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.
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 = "*"
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
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:
// 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