Quick Start
Prefer Python? See Quick Start (Python)
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:
src/main.rs- Your code (we'll customize this)horus.toml- Project config (name, version, dependencies).horus/- Build cache (generatedCargo.toml,target/, packages/)
Note:
.horus/is managed automatically — it contains the generatedCargo.tomland build artifacts. Your dependencies live inhorus.tomland are pinned inhorus.lock. See Configuration Reference for details.
Step 2: Write the Code
Replace the generated src/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);
// WARNING: sleep() in tick() blocks the entire scheduler thread.
// In production, use .rate(1.hz()) on the scheduler instead.
std::thread::sleep(Duration::from_secs(1));
}
// SAFETY: shutdown() ensures the sensor stops cleanly and logs final state.
// Always implement shutdown() — the scheduler calls it on Ctrl+C / signal.
fn shutdown(&mut self) -> Result<()> {
eprintln!("TemperatureSensor shutting down. Last reading: {:.1}°C", self.temperature);
Ok(())
}
}
//============================================
// 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) {
// IMPORTANT: call recv() every tick to drain the topic buffer.
// Skipping ticks causes stale data accumulation.
if let Some(temp) = self.subscriber.recv() {
println!("Temperature: {:.1}°C", temp);
}
}
// SAFETY: shutdown() logs that the monitor is stopping.
// Even non-actuator nodes should implement shutdown() for clean lifecycle tracking.
fn shutdown(&mut self) -> Result<()> {
eprintln!("TemperatureMonitor shutting down.");
Ok(())
}
}
//============================================
// MAIN - Run both nodes
//============================================
fn main() -> Result<()> {
eprintln!("Starting temperature monitoring system...\n");
// Create the scheduler
let mut scheduler = Scheduler::new();
// Execution order: sensor publishes (0) before monitor subscribes (1).
// This guarantees the monitor always sees the latest reading each tick.
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 dependencies from
horus.toml - 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
}
// SAFETY: always implement shutdown() for clean resource release.
fn shutdown(&mut self) -> Result<()> {
Ok(())
}
}
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);
// WARNING: sleep() blocks the scheduler. Use .rate(1.hz()) instead.
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
}
node! {
TemperatureMonitor {
sub { subscriber: f32 -> "temperature" }
tick {
// IMPORTANT: call recv() every tick to consume messages.
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();
// Execution order: sensor (0) publishes before monitor (1) subscribes.
scheduler.add(SensorNode::new()?)
.order(0) // Runs first each tick
.build()?;
scheduler.add(MonitorNode::new()?)
.order(1) // Runs second each tick
.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 (auto-enables RT detection).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) {
// IMPORTANT: call recv() every tick to drain the buffer.
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) {
// IMPORTANT: call recv() every tick to drain the buffer.
if let Some(temp) = self.subscriber.recv() {
// Display
println!("Temperature: {:.1}°C", temp);
// WARNING: file I/O in tick() blocks the scheduler. In production,
// use an async writer or buffer writes to avoid stalling the loop.
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 shared memory, managed automatically by HORUS. You never need to configure paths — horus_sys handles platform differences internally.
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"
A shared memory region exists from a previous run. Clean it up:
horus clean --shm
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.
See Also
- Quick Start (Python) - Build the same app in Python
- Second Application - Build a more advanced project
- Choosing a Language - Rust vs Python comparison
- Tutorials - Guided walkthroughs for common tasks