Quick Start

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:

  • main.rs - Your code (we'll customize this)
  • horus.toml - Project config (name, version, description)
  • Cargo.toml - Rust dependencies (managed by cargo / horus add)
  • .horus/ - Build cache (target/, packages/)

Note: .horus/ is a build cache managed automatically. Your dependencies live in Cargo.toml (Rust) or pyproject.toml (Python), just like normal projects. See Environment Management for details.

Step 2: Write the Code

Replace the generated 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);

        // Wait 1 second before next reading
        std::thread::sleep(Duration::from_secs(1));
    }
}

//============================================
// 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) {
        // Check for new temperature readings
        if let Some(temp) = self.subscriber.recv() {
            println!("Temperature: {:.1}°C", temp);
        }
    }
}

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

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

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

    // Add both nodes using the fluent API
    // order(0) = sensor runs first (highest priority)
    // order(1) = monitor runs second
    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 your Cargo.toml dependencies
  • 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
    }
}

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);
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    }
}

node! {
    TemperatureMonitor {
        sub { subscriber: f32 -> "temperature" }
        tick {
            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();

// order(0) = highest priority (runs first)
scheduler.add(SensorNode::new()?)
    .order(0)
    .build()?;

// order(1) = lower priority (runs after 0)
scheduler.add(MonitorNode::new()?)
    .order(1)
    .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
  • .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) {
        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) {
        if let Some(temp) = self.subscriber.recv() {
            // Display
            println!("Temperature: {:.1}°C", temp);

            // Save to file
            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 platform-specific shared memory:

  • Linux: /dev/shm/horus/
  • macOS: /tmp/horus/
  • Windows: %TEMP%\horus\

Check it out (Linux):

ls -lh /dev/shm/horus/

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"

The shared memory file exists from a previous run. Remove it:

# Linux
rm -f /dev/shm/horus/topic_temperature
# macOS
rm -f /tmp/horus/topic_temperature

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.