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:
- Node — a struct with a
tick()method. The scheduler callstick()over and over; that is your program. - 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. - 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:
- Installation — get the
horusbinary - Quick Start (this page) — node, topic, scheduler
- Second Application — several nodes, several rates
- Choosing a Language — Rust, Python or C++
- 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:
- Sensor - Generates temperature readings
- 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.tomlfrom yourhorus.tomlusing 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.tomlfrom 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.
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 — neversleep()insidetick(), 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:
- node! Macro - Eliminate boilerplate code
- CLI Reference - All the
horuscommands - Monitor - Monitor your application visually
See More Examples:
- Examples - Real applications you can run
- Multi-Language - Use Python instead
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:
- Use the node! macro to eliminate boilerplate
- Run the examples to see real applications
- Open the monitor to monitor your system
For issues, see the Troubleshooting Guide.