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:

Loading diagram...
Nodes communicate through Topics, not direct calls

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:

Loading diagram...
Node lifecycle: init once, tick repeatedly, shutdown once
PhaseMethodWhenUse For
Startupinit()Once, before first tickOpen files, connect to hardware, create topics
Runningtick()Every scheduler cycleRead sensors, compute, send commands
Cleanupshutdown()Once, on exitStop 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