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:
- 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:
main.rs- Your code (we'll customize this)horus.toml- Project config (name, version, description)Cargo.toml- Rust dependencies (managed bycargo/horus add).horus/- Build cache (target/, packages/)
Note:
.horus/is a build cache managed automatically. Your dependencies live inCargo.toml(Rust) orpyproject.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.tomldependencies - 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:
- 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/ - 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:
- 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.