Quick Start

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

The three concepts you need

HORUS has a large surface — execution classes, budgets and deadlines, miss policies, safe states, services, actions, transforms, drivers. None of it is required to finish this page. Three concepts carry the whole example:

  1. Node — a struct with a tick() method. The scheduler calls tick() over and over; that is your program.
  2. Topic — a named channel. Topic::new("temperature") opens it, send() writes, recv() reads. Any node in any language that opens the same name is connected to you.
  3. Scheduler — owns your nodes and calls their ticks.

The code below adds exactly two optional knobs, and you can delete both: .order(n) sets execution order within a tick (default 0) and .rate(hz) moves a node onto its own real-time thread (without it, a node ticks best-effort at the scheduler's default 100 Hz). Delete them and the program still runs — the temperature just scrolls at 100 Hz instead of once a second.

The path through Getting Started

If you read the docs in one order, read them in this one:

  1. Installation — get the horus binary
  2. Quick Start (this page) — node, topic, scheduler
  3. Second Application — several nodes, several rates
  4. Choosing a Language — Rust, Python or C++
  5. Common Mistakes — the nine things that bite first

Everything after that is reference material you can reach for when a specific need shows up.

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 - Dependencies and project metadata
  • .gitignore - Ignores the auto-managed build artifacts
  • .horus/ - Auto-managed environment (local workspace + global cache)

Note: .horus/ is automatically managed. For Rust projects, HORUS generates .horus/Cargo.toml from your horus.toml using path references (no source copying). See Environment Management for details.

Step 2: Write the Code

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

use horus::prelude::*;

//===========================================
// 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) -> &'static str {
        "TemperatureSensor"
    }

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

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

//============================================
// 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) -> &'static 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
    // rate(1 Hz) puts each node on its own real-time thread, ticking once
    // per second. They run concurrently — order() only sequences nodes that
    // share the main tick loop, which neither of these does.
    // The topic still connects them: the monitor sees whatever the sensor
    // has published by the time it reads.
    scheduler.add(TemperatureSensor::new()?)
        .order(0)
        .rate(1_u64.hz())
        .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:

  • Scan dependencies from horus.toml
  • 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.

ℹ️Scheduler lines before the first reading are normal

Several lines of scheduler and real-time output appear above the temperatures. The one that alarms people is:

[RT-thread] Could not set SCHED_FIFO: Permission denied: SCHED_FIFO requires
CAP_SYS_NICE or root (continuing with normal priority)

That is not a failure — the message says so at the end. Calling .rate() promotes a node to the real-time class, and HORUS then asks the kernel for SCHED_FIFO, which an unprivileged user cannot have. It falls back to normal scheduling and your program runs exactly as written; on a desktop that is fine, since nothing here depends on real-time priority. Grant the capability with horus setup-rt when you get to a robot that does.

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) -> &'static str {
        "TemperatureSensor"
    }

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

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(freq) - Set node-specific tick rate, e.g. .rate(100_u64.hz()) (auto-derives budget = 80% of period and deadline = 95% of period). This is how you pace a node — never sleep() inside tick(), which stalls every other node in the cycle.
  • .budget(dur) / .deadline(dur) - Override the auto-derived RT budget/deadline (either one implicitly makes the node real-time)
  • .build()? - Validate the configuration and register the node (.done() is a compatibility alias)

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

Split the two nodes into their own files — sensor.rs and monitor.rs, each with its own main() that schedules just that node — then 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_<namespace>/topics/
  • macOS: /tmp/horus_<namespace>/topics/
  • Windows: %TEMP%\horus_<namespace>\topics\

<namespace> defaults to default (so /dev/shm/horus_default/topics/); set HORUS_NAMESPACE to isolate robots.

Check it out (Linux):

ls -lh /dev/shm/horus_default/topics/

Troubleshooting

"Failed to create Topic"

Usually an invalid topic name (allowed characters: a-z, A-Z, 0-9, _, -, ., /; max 255 chars, never empty), a shared-memory permission problem, or /dev/shm being full. Check the topic name, then clear stale shared memory:

horus clean --shm

Nothing prints

Make sure both nodes are added:

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

"Could not set SCHED_FIFO: Permission denied"

.rate(...) marks a node real-time, so the scheduler tries to raise its thread priority at startup. Without CAP_SYS_NICE (or root) it logs this once and continues at normal priority — the example still ticks at 1 Hz. See Real-Time Nodes if you need genuine RT scheduling.

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.