Choosing Your Configuration

HORUS has many configuration options. You don't need most of them. This guide tells you exactly what to use based on what you're building.

Level 0: Just Getting Started

You need nothing. Defaults work:

use horus::prelude::*;

let mut scheduler = Scheduler::new();
scheduler.add(MyNode::new()).build()?;
scheduler.run()?;
import horus

node = horus.Node(name="my_node", tick=my_tick, rate=30)
horus.run(node)

This gives you: 100Hz tick rate, best-effort scheduling, no RT, no safety monitor. Good enough for learning and prototyping.

Level 1: Setting Tick Rates

When you know how fast your nodes should run:

let mut scheduler = Scheduler::new()
    .tick_rate(100_u64.hz());         // scheduler runs at 100Hz

scheduler.add(sensor)
    .order(0)                          // runs first
    .rate(100_u64.hz())                // ticks at 100Hz
    .build()?;

scheduler.add(controller)
    .order(1)                          // runs second
    .rate(100_u64.hz())
    .build()?;

When to add .order(): When execution order matters. Sensor before controller. Controller before motor driver. Lower number = runs first.

When to add .rate(): When your node needs a specific frequency. A camera at 30Hz. A motor controller at 1kHz. Without .rate(), the node ticks at the scheduler's global rate.

Level 2: Separating Workloads

When you have both fast control and slow computation:

// Fast motor control — dedicated RT thread
scheduler.add(motor_ctrl)
    .order(0)
    .rate(1000_u64.hz())               // auto-RT at 1kHz
    .build()?;

// Slow path planning — separate compute pool
scheduler.add(planner)
    .order(5)
    .compute()                         // won't block motor control
    .build()?;

// Network upload — async I/O pool
scheduler.add(telemetry)
    .order(100)
    .async_io()                        // won't block anything
    .rate(1_u64.hz())
    .build()?;

When to add .compute(): For CPU-heavy work (SLAM, planning, ML inference) that takes more than a few milliseconds. Keeps it off the RT thread.

When to add .async_io(): For I/O-bound work (network, files, database). Never blocks anything.

When to add .on("topic"): When a node should only run when new data arrives on a topic, not on a fixed schedule.

Level 3: Safety for Real Robots

When deploying on physical hardware:

let mut scheduler = Scheduler::new()
    .tick_rate(500_u64.hz())
    .prefer_rt()                       // try SCHED_FIFO + mlockall
    .watchdog(500_u64.ms());           // detect frozen nodes

scheduler.add(motor_ctrl)
    .order(0)
    .rate(500_u64.hz())
    .on_miss(Miss::SafeMode)           // enter safe state on deadline miss
    .build()?;

scheduler.add(sensor_driver)
    .order(1)
    .rate(100_u64.hz())
    .failure_policy(FailurePolicy::restart(3, 100_u64.ms()))  // restart on crash
    .build()?;

scheduler.add(logger)
    .order(100)
    .async_io()
    .failure_policy(FailurePolicy::Ignore)  // never crash for logging
    .build()?;

When to add .prefer_rt(): When running on a real robot. Enables OS-level RT features for better timing.

When to add .watchdog(): When you need to detect nodes that hang or deadlock.

When to add .on_miss(): For safety-critical nodes. What happens when a motor controller misses its deadline?

PolicyUse When
Miss::WarnDefault. Non-critical nodes.
Miss::SkipVideo encoding, logging. Drop a frame, keep going.
Miss::SafeModeMotor controllers. Reduce speed or hold position.
Miss::StopEmergency systems. Stop everything.

When to add .failure_policy(): To control what happens when tick() panics or errors.

PolicyUse When
FatalMotor control, safety. Stop if broken.
Restart(n, backoff)Sensor drivers. Reconnect after crash.
Skip(n, cooldown)Logging, telemetry. Tolerate failures.
IgnoreDebug output. Partial results are fine.

Level 4: Production Deployment

For robots running unattended:

let mut scheduler = Scheduler::new()
    .tick_rate(500_u64.hz())
    .prefer_rt()
    .watchdog(500_u64.ms())
    .blackbox(64)                      // 64MB crash recorder
    .max_deadline_misses(50);          // emergency stop after 50 misses

scheduler.add(safety_monitor)
    .order(0)
    .rate(1000_u64.hz())
    .priority(99)                      // highest OS priority
    .core(2)                           // pinned to CPU core 2
    .watchdog(5_u64.ms())              // tight per-node watchdog
    .on_miss(Miss::Stop)
    .failure_policy(FailurePolicy::Fatal)
    .build()?;

When to add .blackbox(): For crash forensics. "Why did the robot stop at 3 AM?"

When to add .priority(): When you need OS-level thread priority (SCHED_FIFO). Only for RT nodes.

When to add .core(): When you need CPU pinning. Prevents cache thrashing on multi-core systems.

When to add per-node .watchdog(): When different nodes need different timeout tolerances. A safety monitor needs 5ms. A logger can tolerate 5 seconds.

Level 5: Simulation and Testing

For deterministic, reproducible behavior:

let mut scheduler = Scheduler::new()
    .deterministic(true)               // virtual clock + dependency ordering
    .tick_rate(100_u64.hz());

// Use horus::dt() instead of Instant::now()
// Use horus::rng() instead of rand::random()
// Same code works in both normal and deterministic mode

for _ in 0..1000 {
    scheduler.tick_once()?;            // exact same output every run
}

When to add .deterministic(true): Simulation, testing, CI pipelines. NOT for real robots (virtual time can't drive hardware).

When to add .with_recording(): To capture a session for later replay and debugging.

Decision Flowchart

Are you learning / prototyping?
  → Level 0: Scheduler::new(), no options

Do you have multiple nodes at different rates?
  → Level 1: add .order() and .rate()

Do you have slow compute (SLAM, planning) alongside fast control?
  → Level 2: add .compute() for heavy work

Are you running on a physical robot?
  → Level 3: add .prefer_rt(), .watchdog(), .on_miss(), .failure_policy()

Is the robot running unattended / in production?
  → Level 4: add .blackbox(), .priority(), .core()

Do you need reproducible tests or simulation?
  → Level 5: add .deterministic(true)

Quick Reference: All Options

Scheduler

MethodLevelWhat It Does
.tick_rate(freq)1Global tick rate
.prefer_rt()3Enable OS RT features
.require_rt()4Panic if RT unavailable
.watchdog(Duration)3Detect frozen nodes
.blackbox(size_mb)4Crash recorder
.max_deadline_misses(n)4Emergency stop threshold
.cores(&[usize])4CPU core pinning
.deterministic(bool)5Reproducible execution
.with_recording()5Record for replay
.verbose(bool)3Control log output
.telemetry(endpoint)4Export metrics

Node Builder

MethodLevelWhat It Does
.order(u32)1Execution priority
.rate(Frequency)1Tick rate (auto-RT)
.compute()2CPU-bound thread pool
.async_io()2I/O-bound async pool
.on("topic")2Event-triggered
.on_miss(Miss)3Deadline miss response
.failure_policy(Policy)3Crash recovery
.budget(Duration)3Override tick budget
.deadline(Duration)3Override deadline
.priority(i32)4OS thread priority
.core(usize)4CPU core pinning
.watchdog(Duration)4Per-node watchdog
.build()0Finalize (always needed)