Quick Start

Prefer Python? See Quick Start (Python)

This tutorial demonstrates building a temperature monitoring system with HORUS. Estimated time: 10 minutes.

What We're Building

A system with two components:

  1. Sensor - Generates temperature readings
  2. Monitor - Displays the readings

They'll communicate using HORUS's ultra-fast shared memory.

Step 1: Create a New Project

# Create a new HORUS project
horus new temperature-monitor

# Select options in the interactive prompt:
# Language: Rust (option 2)
# Use macros: No (we'll learn the basics first)

cd temperature-monitor

This creates:

  • src/main.rs - Your code (we'll customize this)
  • horus.toml - Project config (name, version, dependencies)
  • .horus/ - Build cache (generated Cargo.toml, target/, packages/)

Note: .horus/ is managed automatically — it contains the generated Cargo.toml and build artifacts. Your dependencies live in horus.toml and are pinned in horus.lock. See Configuration Reference for details.

Step 2: Write the Code

Replace the generated src/main.rs with this complete example:

use horus::prelude::*;
use std::time::Duration;

//===========================================
// SENSOR NODE - Generates temperature data
//===========================================

struct TemperatureSensor {
    publisher: Topic<f32>,
    temperature: f32,
}

impl TemperatureSensor {
    fn new() -> Result<Self> {
        Ok(Self {
            publisher: Topic::new("temperature")?,
            temperature: 20.0,
        })
    }
}

impl Node for TemperatureSensor {
    fn name(&self) -> &str {
        "TemperatureSensor"
    }

    fn tick(&mut self) {
        // Simulate temperature change
        self.temperature += 0.1;

        // Send the reading
        self.publisher.send(self.temperature);

        // WARNING: sleep() in tick() blocks the entire scheduler thread.
        // In production, use .rate(1.hz()) on the scheduler instead.
        std::thread::sleep(Duration::from_secs(1));
    }

    // SAFETY: shutdown() ensures the sensor stops cleanly and logs final state.
    // Always implement shutdown() — the scheduler calls it on Ctrl+C / signal.
    fn shutdown(&mut self) -> Result<()> {
        eprintln!("TemperatureSensor shutting down. Last reading: {:.1}°C", self.temperature);
        Ok(())
    }
}

//============================================
// MONITOR NODE - Displays temperature data
//============================================

struct TemperatureMonitor {
    subscriber: Topic<f32>,
}

impl TemperatureMonitor {
    fn new() -> Result<Self> {
        Ok(Self {
            subscriber: Topic::new("temperature")?,
        })
    }
}

impl Node for TemperatureMonitor {
    fn name(&self) -> &str {
        "TemperatureMonitor"
    }

    fn tick(&mut self) {
        // IMPORTANT: call recv() every tick to drain the topic buffer.
        // Skipping ticks causes stale data accumulation.
        if let Some(temp) = self.subscriber.recv() {
            println!("Temperature: {:.1}°C", temp);
        }
    }

    // SAFETY: shutdown() logs that the monitor is stopping.
    // Even non-actuator nodes should implement shutdown() for clean lifecycle tracking.
    fn shutdown(&mut self) -> Result<()> {
        eprintln!("TemperatureMonitor shutting down.");
        Ok(())
    }
}

//============================================
// MAIN - Run both nodes
//============================================

fn main() -> Result<()> {
    eprintln!("Starting temperature monitoring system...\n");

    // Create the scheduler
    let mut scheduler = Scheduler::new();

    // Execution order: sensor publishes (0) before monitor subscribes (1).
    // This guarantees the monitor always sees the latest reading each tick.
    scheduler.add(TemperatureSensor::new()?)
        .order(0)
        .build()?;

    scheduler.add(TemperatureMonitor::new()?)
        .order(1)
        .build()?;

    // Run forever (press Ctrl+C to stop)
    scheduler.run()?;

    Ok(())
}

Step 3: Run It!

horus run --release

HORUS will automatically:

  • Read project config from horus.toml
  • Build with Cargo using dependencies from horus.toml
  • Execute your program

You'll see:

Starting temperature monitoring system...

Temperature: 20.1°C
Temperature: 20.2°C
Temperature: 20.3°C
Temperature: 20.4°C
...

Press Ctrl+C to stop.

Understanding the Code

The Topic - Communication Channel

// Create a publisher (sends data)
publisher: Topic::new("temperature")?

// Create a subscriber (receives data)
subscriber: Topic::new("temperature")?

Both use the same topic name ("temperature"). The Topic manages all shared memory operations automatically.

The Node Trait - Component Lifecycle

Each component implements the Node trait:

impl Node for TemperatureSensor {
    // Give your node a name
    fn name(&self) -> &str {
        "TemperatureSensor"
    }

    // This runs repeatedly
    fn tick(&mut self) {
        // Your logic here
    }

    // SAFETY: always implement shutdown() for clean resource release.
    fn shutdown(&mut self) -> Result<()> {
        Ok(())
    }
}

Shortcut: The node! Macro

The same two nodes can be written with far less boilerplate using the node! macro:

use horus::prelude::*;

node! {
    TemperatureSensor {
        pub { publisher: f32 -> "temperature" }
        data { temperature: f32 = 20.0 }
        tick {
            self.temperature += 0.1;
            self.publisher.send(self.temperature);
            // WARNING: sleep() blocks the scheduler. Use .rate(1.hz()) instead.
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    }
}

node! {
    TemperatureMonitor {
        sub { subscriber: f32 -> "temperature" }
        tick {
            // IMPORTANT: call recv() every tick to consume messages.
            if let Some(temp) = self.subscriber.recv() {
                println!("Temperature: {:.1}°C", temp);
            }
        }
    }
}

The macro generates the struct, constructor, and Node trait implementation automatically. Both approaches produce identical runtime behavior — choose whichever you prefer. See the node! Macro Guide for the full syntax.

The Scheduler - Running Everything

The scheduler runs your nodes in priority order:

let mut scheduler = Scheduler::new();

// Execution order: sensor (0) publishes before monitor (1) subscribes.
scheduler.add(SensorNode::new()?)
    .order(0)    // Runs first each tick
    .build()?;

scheduler.add(MonitorNode::new()?)
    .order(1)    // Runs second each tick
    .build()?;

// Run forever
scheduler.run()?;

The fluent API lets you chain configuration:

  • .order(n) - Set execution priority (lower = runs first)
  • .rate(n.hz()) - Set node-specific tick rate (auto-enables RT detection)
  • .budget(n.us()) - Set execution time budget (auto-enables RT)
  • .build() - Finish and register the node

Running Nodes in Separate Processes

The example above runs both nodes in a single process. HORUS uses a flat namespace (like ROS), so multi-process communication works automatically!

Running in Separate Terminals

Just run each file in a different terminal - they automatically share topics:

# Terminal 1: Run sensor
horus run sensor.rs

# Terminal 2: Run monitor (automatically connects!)
horus run monitor.rs

Both use the same topic name ("temperature") → communication works automatically!

Using Glob Pattern

Run multiple files together:

horus run "*.rs"  # All Rust files run as separate processes

[TIP] See Topic for details on the shared memory architecture.

Next Steps

Add More Features

Try modifying the code:

1. Add a temperature alert:

impl Node for TemperatureMonitor {
    fn tick(&mut self) {
        // IMPORTANT: call recv() every tick to drain the buffer.
        if let Some(temp) = self.subscriber.recv() {
            println!("Temperature: {:.1}°C", temp);

            // Alert if temperature exceeds threshold
            if temp > 25.0 {
                eprintln!("WARNING: Temperature too high!");
            }
        }
    }
}

2. Add a second sensor:

// In main():
scheduler.add(HumiditySensor::new()?)
    .order(0)
    .build()?;
scheduler.add(HumidityMonitor::new()?)
    .order(1)
    .build()?;

3. Save data to a file:

use std::fs::OpenOptions;
use std::io::Write;

impl Node for TemperatureMonitor {
    fn tick(&mut self) {
        // IMPORTANT: call recv() every tick to drain the buffer.
        if let Some(temp) = self.subscriber.recv() {
            // Display
            println!("Temperature: {:.1}°C", temp);

            // WARNING: file I/O in tick() blocks the scheduler. In production,
            // use an async writer or buffer writes to avoid stalling the loop.
            let mut file = OpenOptions::new()
                .create(true)
                .append(true)
                .open("temperature.log")
                .unwrap();
            writeln!(file, "{:.1}", temp).ok();
        }
    }
}

Learn More Concepts

Now that you've built your first app, learn the details:

Core Concepts:

  • Nodes - Deep dive into the Node pattern
  • Topic - How ultra-fast communication works
  • Scheduler - Priority-based execution

Make Development Easier:

See More Examples:

Common Questions

Do I need Box::new()?

No! The fluent API handles everything automatically:

scheduler.add(MyNode::new())
    .order(0)
    .build()?;

Can I use async/await?

Nodes use simple synchronous code — tick() is called repeatedly by the scheduler's main loop. This keeps things simple and deterministic, which is important for real-time robotics.

How do I stop the application?

Press Ctrl+C. The scheduler handles graceful shutdown automatically.

Where does the data go?

Data is stored in shared memory, managed automatically by HORUS. You never need to configure paths — horus_sys handles platform differences internally.

Troubleshooting

"Failed to create Topic"

Another program might be using the same topic name. Pick a unique name:

Topic::new("temperature_sensor_1")?

"Address already in use"

A shared memory region exists from a previous run. Clean it up:

horus clean --shm

Or use a different topic name.

Nothing prints

Make sure both nodes are added:

scheduler.add(Sensor::new()?)
    .order(0)
    .build()?;
scheduler.add(Monitor::new()?)
    .order(1)
    .build()?;

What You've Learned

How to create a HORUS project The Node trait pattern Using Topic for communication Running multiple nodes with a Scheduler Sending and receiving messages

Ready for More?

Your next steps:

  1. Use the node! macro to eliminate boilerplate
  2. Run the examples to see real applications
  3. Open the monitor to monitor your system

For issues, see the Troubleshooting Guide.

See Also