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 the sensor on its own real-time thread, ticking once
// per second. The monitor sets no .rate(), so it stays best-effort and
// ticks on the scheduler's main loop at the default 100 Hz. order()
// sequences main-loop nodes only until they publish or subscribe — after
// that the scheduler orders them by the topics they share.
// 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. On a robot that does depend on RT priority, run horus setup-rt: it writes an
rtprio 99 / memlock unlimited limits entry for your user, and offers to install a
PREEMPT_RT kernel. To give only this one binary the capability instead,
sudo setcap cap_sys_nice+ep $(which horus).
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 works out what depends on what, from the topics your nodes publish and subscribe to. Here the monitor subscribes to what the sensor publishes, so the sensor runs first:
let mut scheduler = Scheduler::new();
// publishes "robot.sensor"
scheduler.add(SensorNode::new()?)
.order(0)
.build()?;
// subscribes to "robot.sensor" — so it runs after the sensor
scheduler.add(MonitorNode::new()?)
.order(1)
.build()?;
// Run forever
scheduler.run()?;
The scheduler builds its dependency graph from the topic metadata that exists when it
starts — that is, from send()/recv() calls made during init(). Registration is
lazy: Topic::new registers nothing, only the first send() or recv() does.
So if your nodes construct their topics in the constructor and first use them inside
tick() — the common shape — there is no metadata at startup and the graph falls back to
.order() tiers for tick 1. It does not stay there: those first send()/recv()
calls register the topics, and at the start of tick 2 the scheduler rebuilds the graph
from them and keeps it. From that point the pub/sub edges decide the order and .order()
no longer sequences those nodes.
Use .deterministic(true) if you need an order that holds for the whole run.
A node that sends or receives during init() skips even that first tick: the graph has
its edges from the start, so .order() never decides anything for it. Either way, once
the graph exists, independent nodes run concurrently whatever their numbers.
It always matters on the RT, compute and async I/O executors, which start their pools sorted by it.
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.