Quick Start (Python)

Looking for Rust? See Quick Start (Rust).

Prerequisites

  • HORUS installed with horus --help working
  • Python 3.9+ with HORUS bindings (python3 -c "import horus" works)
  • A terminal and text editor

Installing the Python Bindings

Install the horus Python package from PyPI:

pip install horus-robotics

If you are building from source (e.g., developing HORUS itself or need unreleased features), use maturin instead:

cd horus_py
maturin develop --release

maturin develop --release compiles the Rust PyO3 bindings and installs them into your active Python environment. The --release flag enables optimizations — omit it only during development iteration where compile speed matters more than runtime speed.

Verify the installation:

python3 -c "import horus; print('horus OK')"

What You'll Build

A temperature monitoring system with two nodes:

  1. Sensor — generates temperature readings and publishes them
  2. Monitor — subscribes to the readings and displays them

The nodes communicate through HORUS's shared-memory Topics — the same zero-copy IPC as Rust, accessible from Python.

Time estimate: ~10 minutes

Step 1: Create a New Project

horus new temperature-monitor -p
cd temperature-monitor

You should see three items in the project directory:

  • src/main.py — your application code
  • horus.toml — project configuration
  • .horus/ — generated build files (managed automatically)

Project language flags for horus new:

FlagLanguageEntry pointDescription
-pPythonsrc/main.pyPython project (what we're using here)
-rRustsrc/main.rsRust project with manual node setup
-mRustsrc/main.rsRust project with the node! macro for concise definitions

All three generate a horus.toml manifest and .horus/ build directory. The only difference is the language-specific entry point and generated build files (.horus/pyproject.toml for Python, .horus/Cargo.toml for Rust).

Step 2: Write the Code

Replace the contents of src/main.py with the following:

import horus

# ── Sensor Node ──────────────────────────────────────────────
# Publishes a simulated temperature reading every second.

def make_sensor():
    temp = [20.0]  # mutable state via closure

    def tick(node):
        temp[0] += 0.1
        node.send("temperature", temp[0])

    return horus.Node(name="TemperatureSensor", tick=tick, rate=1, order=0,
                      pubs=["temperature"])

# ── Monitor Node ─────────────────────────────────────────────
# Subscribes to temperature readings and prints them.

def monitor_tick(node):
    temp = node.recv("temperature")
    if temp is not None:
        print(f"Temperature: {temp:.1f}°C")

monitor = horus.Node(name="TemperatureMonitor", tick=monitor_tick, rate=1, order=1,
                     subs=["temperature"])

# ── Run ──────────────────────────────────────────────────────

print("Starting temperature monitoring system...\n")
horus.run(make_sensor(), monitor)

Step 3: Run It

horus run

You should see output like:

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.

Step 3.5: Inspect Your System

While your system is running (restart it with horus run in one terminal), open a second terminal in the same project directory and try these debugging tools:

# List all active topics
horus topic list

You should see temperature in the list — this is the shared memory topic your sensor is publishing to.

# Watch messages on a topic in real time
horus topic echo temperature

This prints every message as it's published. You'll see the temperature values streaming. Press Ctrl+C to stop.

# Measure the actual publish rate
horus topic hz temperature

This shows the real frequency of messages. Since your sensor runs at rate=1, you should see ~1.0 Hz.

These tools are your first line of defense when debugging. If a subscriber isn't receiving data, check horus topic list to verify the topic exists, then horus topic echo to verify data is flowing. If the rate looks wrong, horus topic hz tells you exactly what's happening.

Always use horus run — do NOT run python src/main.py directly.

horus run sets up the SHM namespace, environment variables, and build pipeline before executing your code. Running python src/main.py directly skips all of this, which means:

  • Topics won't connect to other processes (no SHM namespace)
  • Environment variables like HORUS_PROJECT_DIR won't be set
  • Dependencies declared in horus.toml won't be resolved
  • The .horus/ build pipeline is bypassed entirely

If you need to pass arguments, use: horus run -- --your-flag value

Step 4: Understand the Key Patterns

You just used three core HORUS concepts:

Node — Component Definition

Each component is a horus.Node with a tick function:

def tick(node):
    node.send("temperature", value)

sensor = horus.Node(
    name="TemperatureSensor",
    tick=tick,
    rate=1,        # tick frequency in Hz
    order=0,       # execution priority (lower = runs first)
    pubs=["temperature"],  # declare published topics
)

The tick function receives the node instance and is called every cycle. State lives in the closure or as class attributes.

Topics — Communication

# Send data to a topic
node.send("temperature", value)

# Receive data from a topic (returns None if no messages)
temp = node.recv("temperature")

Both use the same topic name. HORUS manages shared memory automatically — same zero-copy IPC as Rust.

horus.run() — One-Liner Execution

horus.run(sensor, monitor)

Creates a scheduler, adds all nodes, and runs until Ctrl+C. For more control, use horus.Scheduler directly.

Troubleshooting

SymptomCauseFix
ModuleNotFoundError: horusPython bindings not installedRun pip install horus-robotics. For source builds: cd horus_py && maturin develop --release
Failed to create TopicStale shared memory from a previous runRun horus clean --shm
Nothing printsMonitor added but sensor missingEnsure both nodes are passed to horus.run()
Topics not visible to other processesRunning python src/main.py directlyAlways use horus run — it sets up the SHM namespace. See the warning above
Permission denied on SHM filesShared memory permissions mismatchRun horus doctor to diagnose. On Linux, check /dev/shm/ permissions
Output looks slow or laggyRunning in debug modeUse horus run --release for optimized builds. Debug builds are 10-50x slower for compute-heavy code
horus: command not foundHORUS not in PATHRe-run the installer or add ~/.horus/bin to your PATH
TypeError in tick functionWrong tick function signatureTick functions must accept exactly one argument: def tick(node). The scheduler passes the node instance automatically

Key Takeaways

  • Nodes are created with horus.Node(name, tick, rate, order, pubs, subs)
  • node.send(topic, data) publishes data, node.recv(topic) subscribes (returns None if empty)
  • horus.run(*nodes) is the one-liner to run everything
  • State in tick functions lives in closures (lists for mutability) or class instances
  • The Python API uses the same shared-memory Topics as Rust — no performance penalty for IPC

Next Steps

Beyond the Basics

You've seen Nodes, Topics, and horus.run(). HORUS has much more — here's what to explore next:

FeatureWhat it doesGuide
Execution classesRun nodes as RT, compute-bound, event-driven, or async I/OExecution Classes
Watchdog & safetyDetect frozen nodes, enforce deadlines, graduated degradationSafety Policies (Python)
BlackBoxFlight recorder for post-mortem crash analysisBlackBox
Deterministic modeReproducible execution for simulation and CIDeterministic Mode
Record & ReplayTick-perfect replay for reproducing field bugsRecord & Replay
Fault tolerancePer-node failure policies (restart, skip, fatal)Circuit Breaker
Framework clockhorus.now(), horus.dt(), horus.budget_remaining()Real-Time (Python)
Progressive configFrom prototype to production in 5 levelsChoosing Your Configuration

See Also