Quick Start

Let's build a simple temperature monitoring system to see HORUS in action. This will take about 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.yaml - Dependencies and project metadata
  • .horus/ - Auto-managed environment (local workspace + global cache)

Note: .horus/ is automatically managed. For Rust projects, HORUS generates Cargo.toml from your horus.yaml using path references (no source copying). 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: Hub<f32>,
    temperature: f32,
}

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

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

    fn tick(&mut self, ctx: Option<&mut NodeInfo>) {
        // Simulate temperature change
        self.temperature += 0.1;

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

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

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

struct TemperatureMonitor {
    subscriber: Hub<f32>,
}

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

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

    fn tick(&mut self, ctx: Option<&mut NodeInfo>) {
        // Check for new temperature readings
        if let Some(temp) = self.subscriber.recv(ctx) {
            println!("Temperature: {:.1}°C", temp);
        }
    }
}

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

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

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

    // Register both nodes
    // Priority 0 = sensor runs first
    // Priority 1 = monitor runs second
    // Logging enabled (Some(true)) to see message flow
    scheduler.register(
        Box::new(TemperatureSensor::new()?),
        0,
        Some(true)
    );
    scheduler.register(
        Box::new(TemperatureMonitor::new()?),
        1,
        Some(true)
    );

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

    Ok(())
}

Step 3: Run It!

horus run --release

HORUS will automatically:

  • Scan dependencies from horus.yaml
  • Generate .horus/Cargo.toml from dependencies
  • Compile with Cargo (optimized)
  • 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 Hub - Communication Channel

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

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

Both use the same topic name ("temperature"). The Hub handles all the shared memory magic automatically!

The Node Trait - Component Lifecycle

Each component implements the Node trait:

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

    // This runs repeatedly
    fn tick(&mut self, ctx: Option<&mut NodeInfo>) {
        // Your logic here
    }
}

The Scheduler - Running Everything

The scheduler runs your nodes in priority order:

let mut scheduler = Scheduler::new();

// Priority 0 = highest (runs first)
scheduler.register(Box::new(SensorNode::new()?), 0, Some(true));

// Priority 1 = lower (runs after priority 0)
scheduler.register(Box::new(MonitorNode::new()?), 1, Some(true));

// Run forever
scheduler.tick_all()?;

Next Steps

Add More Features

Try modifying the code:

1. Add a temperature alert:

impl Node for TemperatureMonitor {
    fn tick(&mut self, ctx: Option<&mut NodeInfo>) {
        if let Some(temp) = self.subscriber.recv(ctx) {
            println!("Temperature: {:.1}°C", temp);

            // Alert if too hot!
            if temp > 25.0 {
                println!(" WARNING: Temperature too high!");
            }
        }
    }
}

2. Add a second sensor:

// In main():
scheduler.register(Box::new(HumiditySensor::new()?), 0, Some(true));
scheduler.register(Box::new(HumidityMonitor::new()?), 1, Some(true));

3. Save data to a file:

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

impl Node for TemperatureMonitor {
    fn tick(&mut self, ctx: Option<&mut NodeInfo>) {
        if let Some(temp) = self.subscriber.recv(ctx) {
            // 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
  • Hub - How ultra-fast communication works
  • Scheduler - Priority-based execution

Make Development Easier:

See More Examples:

Common Questions

Why do I need Box::new()?

Nodes are stored polymorphically (different types in one collection). Box::new() puts them on the heap so they can be stored together.

What's Option<&mut NodeInfo>?

It's an optional context that provides:

  • Logging functions
  • Timing information
  • Node metadata

Pass it along to send() and recv() for automatic logging.

Can I use async/await?

The scheduler uses tokio internally, but nodes use simple sync code. This keeps things simple and predictable.

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 at /dev/shm/horus/. Check it out:

ls -lh /dev/shm/horus/

Troubleshooting

"Failed to create Hub"

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

Hub::new("temperature_sensor_1")?

"Address already in use"

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

rm -f /dev/shm/horus/topic_temperature

Or use a different topic name.

Nothing prints

Make sure both nodes are registered:

scheduler.register(Box::new(Sensor::new()?), 0, Some(true));
scheduler.register(Box::new(Monitor::new()?), 1, Some(true));

What You've Learned

How to create a HORUS project The Node trait pattern Using Hub 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 dashboard to monitor your system

Having issues? See the Troubleshooting Guide for help with common problems.

Keep building!