Nodes: The Building Blocks
For the full reference with all lifecycle methods, priority levels, and communication patterns, see Nodes — Full Reference.
What is a Node?
A node is one piece of your robot's software. Each node does one job:
- A SensorNode reads the camera or IMU
- A ControlNode moves the motors
- A SafetyNode prevents collisions
- A PlannerNode decides where to go
Nodes are independent — if one crashes, the others keep running. This is critical for robots: a camera driver bug shouldn't stop your emergency brake.
Your First Node
Every node implements the Node trait. The only required method is tick() — your main logic that runs every cycle:
use horus::prelude::*;
struct Heartbeat;
impl Node for Heartbeat {
fn name(&self) -> &str { "Heartbeat" }
fn tick(&mut self) {
println!("Robot is alive!");
}
}
That's it. The scheduler calls tick() repeatedly — you don't manage loops, threads, or timing.
In Python:
import horus
def heartbeat_tick(node):
print("Robot is alive!")
heartbeat = horus.Node(name="Heartbeat", tick=heartbeat_tick)
How Nodes Communicate
Nodes don't call each other directly. They send data through Topics — named channels for specific data types:
The sensor doesn't know the monitor exists. It just publishes data. Any number of subscribers can listen — zero coupling between components.
Node Lifecycle
Every node has three phases:
| Phase | Method | When | Use For |
|---|---|---|---|
| Startup | init() | Once, before first tick | Open files, connect to hardware, create topics |
| Running | tick() | Every scheduler cycle | Read sensors, compute, send commands |
| Cleanup | shutdown() | Once, on exit | Stop motors, close connections, save state |
impl Node for MotorController {
fn init(&mut self) -> Result<()> {
self.motor.connect()?; // Open hardware connection
Ok(())
}
fn tick(&mut self) {
if let Some(cmd) = self.commands.recv() {
self.motor.set_velocity(cmd); // Move motor
}
}
fn shutdown(&mut self) -> Result<()> {
self.motor.set_velocity(0.0); // STOP the motor!
self.motor.disconnect()?;
Ok(())
}
}
The shutdown method is especially important for robotics — you always want to stop motors and release hardware safely.
Running a Node
Nodes run inside a Scheduler. You create one, add your nodes, and run:
fn main() -> Result<()> {
let mut scheduler = Scheduler::new();
scheduler.add(SensorNode::new()?)
.order(0) // Runs first
.build()?;
scheduler.add(ControlNode::new()?)
.order(1) // Runs second
.build()?;
scheduler.run()?; // Runs until Ctrl+C
Ok(())
}
The order parameter controls execution sequence: lower numbers run first. This is how you ensure the sensor reads data before the controller processes it.
Key Takeaways
- A node = one component doing one job
- Implement
tick()for your main logic - Use
init()for setup,shutdown()for cleanup - Nodes communicate through Topics, not direct calls
- The Scheduler runs your nodes in order
Next Steps
- Topics: How Nodes Talk — learn about the pub/sub system
- Scheduler: Running Your Nodes — learn execution and timing
- Quick Start — build a complete working example
- Nodes — Full Reference — all lifecycle methods, priority levels, and patterns