# HORUS Documentation (Full) Generated: 2026-03-18T04:24:25.339Z Pages: 161 Source: https://docs.horusrobotics.dev Condensed overview: https://docs.horusrobotics.dev/llms.txt This file contains the complete HORUS documentation in plain text. AI agents can use this to understand the full framework. ## Table of Contents - Getting Started (6 pages) - Learn (4 pages) - Core Concepts (22 pages) - Tutorials (12 pages) - Rust (38 pages) - Rust Guide (2 pages) - Python (10 pages) - Python Guide (2 pages) - Development (11 pages) - Advanced Topics (9 pages) - Standard Library (16 pages) - Plugins (3 pages) - Package Management (4 pages) - Performance (2 pages) - Operations (2 pages) - Reference (5 pages) - Recipes (12 pages) - root (1 pages) ======================================== # SECTION: Getting Started ======================================== --- ## Installation Path: /getting-started/installation Description: Install HORUS on Linux, macOS, or Windows # Installing HORUS This guide covers installing Rust, building HORUS, and verifying the installation. The process takes approximately 10-15 minutes. ## Platform Support HORUS has **native cross-platform support**: | Platform | Status | Notes | |----------|--------|-------| | **Ubuntu 20.04+** | Supported | Recommended for production | | **Ubuntu 22.04+** | Supported | Best performance | | **Debian 11+** | Supported | Tested and working | | **Fedora 36+** | Supported | Use dnf for packages | | **Arch Linux** | Supported | Community maintained | | **Raspberry Pi** | Supported | ARM64 tested on Ubuntu | | **macOS** | Supported | Native `shm_open()` shared memory | | **Windows** | Experimental | Core libraries compile; CLI has unguarded unix imports | | **WSL 2** | Supported | Linux mode in WSL | ## Prerequisites **Required:** - **Operating System**: Linux or macOS (Windows via WSL 2) - **Linux**: Ubuntu 20.04+ recommended (fastest shared memory) - **macOS**: Native support (shared memory managed automatically) - **Windows**: Use WSL 2 (native Windows CLI support is experimental) - **Rust 1.92+**: We'll install this in Step 1 - **Build Tools & System Libraries**: Build tools, pkg-config, and all required development libraries ```bash # Ubuntu/Debian/Raspberry Pi OS - COMPLETE dependencies (copy-paste this!) sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Fedora/RHEL/CentOS sudo dnf groupinstall "Development Tools" sudo dnf install -y pkg-config openssl-devel systemd-devel alsa-lib-devel \ libX11-devel libXrandr-devel libXi-devel libXcursor-devel libXinerama-devel \ wayland-devel wayland-protocols-devel libxkbcommon-devel \ vulkan-devel fontconfig-devel freetype-devel \ libv4l-devel # Arch Linux sudo pacman -S base-devel pkg-config openssl systemd alsa-lib \ libx11 libxrandr libxi libxcursor libxinerama \ wayland wayland-protocols libxkbcommon \ vulkan-icd-loader fontconfig freetype2 \ v4l-utils ``` - **10-15 minutes**: For first-time installation - **Internet connection**: To download dependencies **What these packages do:** - **Core**: `libssl-dev` (networking), `libudev-dev` (device detection), `libasound2-dev` (audio) - **Graphics/GUI**: X11, Wayland libraries (required for monitor) - **Optional**: `libv4l-dev` (camera support), fontconfig (improved text rendering) **Not required:** - Rust (installed in Step 1) - Git (installed in Step 2) - Systems programming experience ## Quick Install (Recommended) Copy and paste these commands into your terminal: ```bash # 1. Install Rust (takes ~2 minutes) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env # 2. Clone HORUS git clone https://github.com/softmata/horus.git cd horus # 3. Run automated installer (takes ~5 minutes) ./install.sh # 4. Verify it works horus --help ``` If the help command displays available commands, installation is complete. Skip to [Next Steps](#next-steps). **What the installer does:** - Builds the `horus` CLI tool and installs it to `~/.cargo/bin/` - Builds core libraries and caches them in `~/.horus/cache/`: - `horus_core` - Core runtime, scheduler, and communication - `horus` - Main framework library - `horus_macros` - Procedural macros for simplified syntax - `horus_library` - Standard message types and transforms - `horus_manager` - CLI tool and project management - Installs Python bindings (`horus-robotics`) if Python 3.9+ is detected - Tracks the installed version for automatic updates - Verifies installation with built-in tests ## Step-by-Step Installation ### Step 1: Install Rust HORUS is built with Rust. No prior Rust experience is required; HORUS also supports Python for application development. **On Linux (or WSL/Docker):** ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Follow the prompts (just press Enter for defaults). **Restart your terminal or run:** ```bash source $HOME/.cargo/env ``` **Verify Rust is installed:** ```bash rustc --version cargo --version ``` You should see version numbers like `rustc 1.92.0` or higher. ### Step 2: Install Git (If You Don't Have It) **Check if you have Git:** ```bash git --version ``` **If not, install it:** ```bash # Ubuntu/Debian sudo apt install git # Fedora sudo dnf install git # Arch Linux sudo pacman -S git ``` ### Step 3: Clone HORUS Download the HORUS source code: ```bash git clone https://github.com/softmata/horus.git cd horus ``` This creates a `horus` directory with all the code. ### Step 4: Run the Installer Use the automated installer to build and install everything: ```bash ./install.sh ``` This script: 1. Builds all HORUS packages (takes ~5 minutes) 2. Installs the `horus` CLI to `~/.cargo/bin/` 3. Installs core libraries to `~/.horus/cache/` 4. Saves version information for updates 5. Runs verification tests The output displays progress with colored indicators. Green checkmarks indicate successful steps. ### Step 5: Verify Installation Test that the `horus` command works: ```bash horus --help ``` You should see a list of available commands like `new`, `run`, `monitor`, etc. ## Python Support Python bindings are **automatically installed** by `./install.sh` if Python 3.9+ is detected. **To verify Python bindings work:** ```bash python3 -c "import horus; print('Python bindings installed successfully')" ``` **Manual installation** (if automatic installation was skipped): ```bash # From the horus repository root: # 1. Install maturin (Python build tool) # Option A: Via Cargo (recommended for Ubuntu 24.04+) cargo install maturin # Option B: Via pip (if not blocked by PEP 668) # pip install maturin # 2. Navigate to Python bindings cd horus_py # 3. Build and install (takes ~3 minutes) maturin develop --release ``` > **Note:** The PyPI package name is `horus-robotics`, but you import it as `import horus` in Python code. See [Python Bindings](/python/api/python-bindings) for complete API documentation and examples. ## Platform-Specific Notes ### Linux (Ubuntu/Debian) Linux installation is straightforward. If build errors occur, install all required packages: ```bash sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev ``` ### Linux (Fedora/RHEL) ```bash sudo dnf groupinstall "Development Tools" sudo dnf install -y pkg-config openssl-devel systemd-devel alsa-lib-devel \ libX11-devel libXrandr-devel libXi-devel libXcursor-devel libXinerama-devel \ wayland-devel wayland-protocols-devel libxkbcommon-devel \ vulkan-devel fontconfig-devel freetype-devel \ libv4l-devel ``` ### Raspberry Pi HORUS fully supports Raspberry Pi (tested on Pi 3, 4, and 5). Install Ubuntu Server or Raspberry Pi OS, then: ```bash # Install all dependencies sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Raspberry Pi specific packages (GPIO, I2C, SPI support) sudo apt install -y libraspberrypi-dev i2c-tools python3-smbus # Enable I2C and SPI (required for sensors) sudo raspi-config # Navigate to: Interface Options → I2C → Enable # Navigate to: Interface Options → SPI → Enable ``` **Performance tips:** - Use 64-bit OS for better performance - Compile with `--release` flag (much faster than debug) - Allocate more RAM to GPU if using camera nodes (in `/boot/config.txt`) ### NVIDIA Jetson Nano HORUS supports Jetson Nano with GPU acceleration for vision tasks: ```bash # Install all dependencies sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Jetson specific packages (GPU acceleration) sudo apt install -y nvidia-jetpack # Verify CUDA is installed nvcc --version ``` **GPU acceleration:** - HORUS can leverage CUDA for vision processing nodes - Ensure JetPack is installed for full GPU support - Camera nodes automatically use hardware H.264 encoding ## Hardware Driver Support (Optional) HORUS includes hardware drivers for real robotics hardware. **These are completely optional** - the default installation does NOT require any hardware packages or drivers. **Key points:** - Default `./install.sh` works without any hardware packages - Nodes automatically use simulation mode without hardware - Hardware features are only compiled when explicitly enabled - You can install HORUS on your laptop and deploy to hardware later **When you need hardware packages:** - Only when you want to enable hardware features with `--features="can-hardware"` etc. - Only for production deployment on actual robots - Never needed for development, testing, or simulation ### Available Hardware Drivers | Driver | Hardware | System Packages | Status | |--------|----------|-----------------|--------| | **SocketCAN** | CAN bus devices | `can-utils` (optional) | Implemented | | **spidev** | SPI devices | None (kernel interface) | Implemented | | **i2cdev** | I2C devices | `i2c-tools` (Pi only) | Implemented | | **Serial** | Serial devices | None (kernel interface) | Implemented | | **GPIO** | GPIO pins | `libraspberrypi-dev` (Pi only) | Implemented | | **PWM** | PWM outputs | `libraspberrypi-dev` (Pi only) | Implemented | | **RealSense** | Intel depth cameras | `librealsense2-dev` | Optional | | **Modbus TCP** | Force-torque sensors | None (pure Rust) | Planned | ### Installing Hardware Driver Packages **For Raspberry Pi GPIO/I2C/SPI/PWM:** ```bash sudo apt install -y libraspberrypi-dev i2c-tools python3-smbus # Enable hardware interfaces sudo raspi-config # Navigate to: Interface Options → I2C → Enable # Navigate to: Interface Options → SPI → Enable # Navigate to: Interface Options → Serial Port → Enable ``` **For CAN bus debugging tools:** ```bash sudo apt install -y can-utils # Setup virtual CAN for testing sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set up vcan0 # Test CAN tools cansend vcan0 123#DEADBEEF candump vcan0 ``` **For Intel RealSense depth cameras:** ```bash # Add Intel RealSense repository sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE sudo add-apt-repository "deb https://librealsense.intel.com/Debian/apt-repo $(lsb_release -cs) main" -u # Install RealSense SDK sudo apt install -y librealsense2-dev librealsense2-utils # Test camera realsense-viewer ``` ### Workflow: Development to Hardware Deployment **Development (on your laptop):** ```bash # 1. Install HORUS normally (no hardware packages needed) ./install.sh # 2. Create and run your project horus new my_robot cd my_robot horus run # Runs in simulation mode ``` **Deployment (on the robot):** ```bash # 1. Install system packages on the robot sudo apt install libraspberrypi-dev i2c-tools can-utils # 2. Enable hardware features and run horus run --enable gpio,i2c --release ``` **Or configure in `horus.toml`:** ```toml enable = ["gpio", "i2c"] ``` Then run: ```bash horus run --release ``` **Available hardware capabilities** (managed by the `horus` CLI via `--enable`): - `can` - SocketCAN support for CAN bus - `spi` - spidev support for SPI devices - `i2c` - i2cdev support for I2C devices - `serial` - serialport support for serial devices - `gpio` - GPIO/PWM support (Raspberry Pi) **Cargo feature flags** (for advanced users building with `cargo` directly): - `macros` - Procedural macros (`node!`, `topics!`) — enabled by default - `telemetry` - Telemetry export for live monitoring — enabled by default - `blackbox` - Flight recorder for post-mortem analysis — enabled by default > **Note:** Most users should use `horus run --enable ` or configure `enable` in `horus.toml` rather than setting cargo features manually. **How hardware fallback works:** 1. Node attempts to initialize hardware (e.g., open `/dev/spidev0.0`) 2. If hardware is unavailable, node logs a warning and uses simulation 3. Status messages show `(HW)` for hardware mode, `(SIM)` for simulation 4. This allows development on laptops and deployment on hardware without code changes ### Hardware in Python With hardware features enabled, you can interact with hardware from Python nodes using HORUS Topics and standard message types: ```python import horus from horus import Imu, CmdVel, Topic imu_sub = Topic(Imu) cmd_pub = Topic(CmdVel) def controller_tick(node): imu_data = imu_sub.recv() if imu_data: cmd_pub.send(CmdVel(linear_x=0.5, angular_z=0.0)) controller = horus.Node(name="controller", tick=controller_tick, rate=50, subs=["imu"], pubs=["cmd_vel"]) horus.run(controller) ``` Additional hardware drivers (CAN bus, SPI, I2C, Dynamixel servos, stepper motors, etc.) are available as **registry packages**. Use `horus search` to find drivers for your hardware. Use `horus search` to find drivers for your specific hardware. ### Windows (Native) HORUS has **experimental Windows support**. The core libraries (`horus_core`, `horus_library`, `horus_sys`) compile on Windows with native `CreateFileMappingW` shared memory. However, the CLI tool (`horus_manager`) currently has unguarded unix-specific code and **does not compile on Windows natively**. **Recommended: Use WSL 2** (see below) for full Windows development. **If you want to try native Windows:** ```powershell # 1. Install Rust (using rustup-init.exe from rustup.rs) # Download and run: https://win.rustup.rs/x86_64 # 2. Clone HORUS (in PowerShell or Git Bash) git clone https://github.com/softmata/horus.git cd horus # 3. Build core libraries only (CLI won't compile yet) cargo build --release -p horus_core -p horus_library ``` > **Note**: Full Windows CLI support is planned. For now, use WSL 2 for the complete HORUS experience including the `horus` CLI tool. **Windows shared memory** (core libraries only): - Uses native `CreateFileMappingW` (kernel-managed, no filesystem paths to configure) - Cross-process communication works natively - CLI tool not yet available on native Windows **Recommended: WSL 2** (full HORUS support) 1. Open PowerShell as Administrator 2. Run: `wsl --install` 3. Restart your computer 4. Follow Linux installation steps inside WSL ### macOS (Native) HORUS has **native macOS support** with shared memory managed automatically: ```bash # 1. Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env # 2. Install Xcode Command Line Tools (if not already installed) xcode-select --install # 3. Clone and build HORUS git clone https://github.com/softmata/horus.git cd horus ./install.sh # 4. Verify installation horus --help ``` **macOS shared memory:** - Uses native POSIX `shm_open()` (kernel-managed, no filesystem paths to configure) - Automatically created on first run - Same API as Linux — no code changes needed **Alternative options** (if native doesn't work for your use case): - **Docker** - Run HORUS in a Linux container - **VMware/Parallels** - Full Linux VM - **Cloud Linux** - Remote development on AWS/DigitalOcean ## Understanding Shared Memory HORUS uses platform-native shared memory for ultra-fast communication between components. The shared memory backend is selected automatically by `horus_sys` — you never need to configure paths manually. | Platform | Backend | Notes | |----------|---------|-------| | Linux | POSIX shared memory (`/dev/shm`) | Fastest, recommended for production | | macOS | `shm_open()` kernel objects | Kernel-managed, no filesystem paths | | Windows | `CreateFileMappingW` pagefile | Kernel-managed (core libraries only, CLI experimental) | Shared memory is created automatically when topics are opened and cleaned up when processes exit. If you need to manually clean up after a crash: ```bash horus clean --shm ``` **Check available shared memory space (Linux):** ```bash df -h /dev/shm ``` You should have at least 256MB. Most systems have 1-2GB. **If you need more space (Linux only):** ```bash # Temporarily increase to 2GB sudo mount -o remount,size=2G /dev/shm # Make permanent: edit /etc/fstab (requires sudo) # Add line: tmpfs /dev/shm tmpfs defaults,size=2G 0 0 ``` ## Updating HORUS To update to the latest version: ```bash # Navigate to HORUS directory cd horus # Pull latest changes and reinstall git pull ./install.sh ``` **To preview changes before updating:** ```bash git fetch git log HEAD..@{u} # See what's new git pull ./install.sh ``` ## Uninstalling To completely remove HORUS: ```bash # Navigate to HORUS directory cd horus # Run the uninstaller ./uninstall.sh ``` **The uninstaller will:** 1. Remove the `horus` CLI binary (`~/.cargo/bin/horus`) 2. Remove all cached libraries (`~/.horus/cache/`) 3. Ask if you want to remove `~/.horus/` (contains auth, config, registry data) 4. Clean up shared memory files (platform-specific paths) 5. Leave project-local `.horus/` directories untouched **Manual uninstall (if needed):** **Linux/macOS:** ```bash # Remove CLI tool cargo uninstall horus # Remove global cache and config rm -rf ~/.horus/ # Remove source code rm -rf ~/horus/ # or wherever you cloned it # Clean up shared memory (optional - HORUS auto-cleans sessions) horus clean --shm ``` **Windows (PowerShell):** ```powershell # Remove CLI tool cargo uninstall horus # Remove global cache and config Remove-Item -Recurse -Force "$env:USERPROFILE\.horus" # Remove source code Remove-Item -Recurse -Force "$env:USERPROFILE\horus" # or wherever you cloned it # Clean up shared memory (optional - HORUS auto-cleans sessions) Remove-Item -Recurse -Force "$env:TEMP\horus" ``` ## Troubleshooting Having installation issues? Try manual recovery: ```bash # Navigate to HORUS source directory cd /path/to/horus # Clean and reinstall cargo clean rm -rf ~/.horus/cache ./install.sh ``` **Common issues:** - **Missing packages**: Install all required system dependencies (see above) - **Raspberry Pi**: Ensure GPIO/I2C packages are installed - **Jetson Nano**: Ensure CUDA/JetPack packages are installed See the **[Troubleshooting & Maintenance Guide](/troubleshooting)** for: - Common installation errors and detailed fixes - System dependency issues - Platform-specific problems - 15+ solved issues with step-by-step solutions ## Next Steps With HORUS installed, proceed to one of the following: 1. **[Quick Start Tutorial](/getting-started/quick-start)** - Build your first HORUS application 2. **[CLI Reference](/development/cli-reference)** - Complete command documentation 3. **[Examples](/rust/examples/basic-examples)** - Sample HORUS applications Recommended: Start with the [Quick Start Tutorial](/getting-started/quick-start). ## See Also - [Quick Start](/getting-started/quick-start) - Build your first HORUS application - [Choosing a Language](/getting-started/choosing-language) - Rust vs Python comparison - [Common Mistakes](/getting-started/common-mistakes) - Avoid frequent pitfalls - [Troubleshooting](/troubleshooting) - Solutions for installation and runtime issues --- ## Quick Start (Python) Path: /getting-started/quick-start-python Description: Build your first HORUS application in 10 minutes — Python edition # Quick Start (Python) > Looking for Rust? See [Quick Start (Rust)](/getting-started/quick-start) This tutorial demonstrates building a temperature monitoring system with HORUS using Python. Estimated time: 10 minutes. ## What We're Building A system with two components: 1. **Sensor** - Generates temperature readings 2. **Monitor** - Displays the readings They'll communicate using HORUS's ultra-fast shared memory. ## Step 1: Create a New Project ```bash # Create a new Python HORUS project horus new temperature-monitor -p cd temperature-monitor ``` This creates: - `main.py` - Your code (we'll customize this) - `horus.toml` - Project config (name, version, description) - `.horus/` - Build cache (packages, virtualenv) ## Step 2: Write the Code Replace `main.py` with this complete example: ```python import horus #=========================================== # SENSOR NODE - Generates temperature data #=========================================== def make_sensor(): temp = [20.0] # mutable state via list 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 - Displays temperature data #============================================ def monitor_tick(node): temp = node.recv("temperature") if temp is not None: print(f"Temperature: {temp:.1f}\u00b0C") monitor = horus.Node(name="TemperatureMonitor", tick=monitor_tick, rate=1, order=1, subs=["temperature"]) #============================================ # MAIN - Run both nodes #============================================ print("Starting temperature monitoring system...\n") # Run forever (press Ctrl+C to stop) horus.run(make_sensor(), monitor) ``` ## Step 3: Run It! ```bash horus run ``` HORUS will automatically: - Detect Python from `main.py` - Set up the virtual environment if needed - Execute your program You'll see: ``` Starting temperature monitoring system... Temperature: 20.1\u00b0C Temperature: 20.2\u00b0C Temperature: 20.3\u00b0C Temperature: 20.4\u00b0C ... ``` Press **Ctrl+C** to stop. ## Understanding the Code ### Topics - Communication Channels ```python # Sending data (in the tick function) node.send("temperature", value) # Receiving data (in the tick function) temp = node.recv("temperature") ``` Both use the same topic name (`"temperature"`). HORUS manages all shared memory operations automatically. Same API as Rust, but Pythonic. ### The Node - Component Definition Each component is a `horus.Node` with a tick function: ```python def make_sensor(): temp = [20.0] def tick(node): temp[0] += 0.1 node.send("temperature", temp[0]) return horus.Node(name="TemperatureSensor", tick=tick, rate=1, pubs=["temperature"]) ``` The `tick` function is your main logic, called every cycle. State lives in the closure (or as a plain class instance passed via `tick=obj.tick`). ### Running Everything ```python # Create nodes with rate and order sensor = make_sensor() monitor = horus.Node(name="TemperatureMonitor", tick=monitor_tick, rate=1, order=1, subs=["temperature"]) # Run all nodes (press Ctrl+C to stop) horus.run(sensor, monitor) ``` Key configuration on `horus.Node(...)`: - `order=n` - Set execution priority (lower = runs first) - `rate=n` - Set tick frequency in Hz - `pubs` / `subs` - Declare topic names ## Running Nodes in Separate Processes The example above runs both nodes in a **single process**. HORUS uses a **flat namespace**, so multi-process communication works automatically. ```bash # Terminal 1: Run sensor horus run sensor.py # Terminal 2: Run monitor (automatically connects!) horus run monitor.py ``` Both use the same topic name (`"temperature"`) and communication just works. ## Next Steps ### Add More Features **1. Add a temperature alert:** ```python def alert_tick(node): temp = node.recv("temperature") if temp is not None: print(f"Temperature: {temp:.1f}\u00b0C") if temp > 25.0: print("WARNING: Temperature too high!") ``` **2. Integrate with NumPy:** ```python import numpy as np import horus def array_tick(node): # Generate 100 sensor readings readings = np.random.normal(22.0, 0.5, 100) node.send("readings", readings.tolist()) sensor_array = horus.Node(name="SensorArray", tick=array_tick, rate=10, pubs=["readings"]) ``` ### Learn More **Core Concepts:** - **[Nodes](/concepts/core-concepts-nodes)** - Deep dive into the Node pattern - **[Topic](/concepts/core-concepts-topic)** - How ultra-fast communication works - **[Scheduler](/concepts/core-concepts-scheduler)** - Priority-based execution **Python-Specific:** - **[Python Bindings](/python/api/python-bindings)** - Full Python API reference - **[Async Nodes](/python/api/async-nodes)** - Using async/await with HORUS - **[ML Utilities](/python/library/ml-utilities)** - TensorFlow/PyTorch integration **See More Examples:** - **[Python Examples](/python/examples)** - Real Python applications - **[Choosing a Language](/getting-started/choosing-language)** - Rust vs Python comparison ## Common Questions ### Can I use pip packages? Yes! Add them to `horus.toml`: ```bash horus add numpy --source pypi horus add torch --source pypi ``` ### Can I use async/await? Yes! HORUS supports async nodes in Python: ```python async def async_tick(node): data = await read_sensor_async() node.send("sensor_data", data) sensor = horus.Node(name="AsyncSensor", tick=async_tick, pubs=["sensor_data"]) ``` See [Async Nodes](/python/api/async-nodes) for details. ### 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 ### "ModuleNotFoundError: No module named 'horus'" Install the HORUS Python bindings: ```bash pip install horus-robotics ``` Or if using a virtual environment: ```bash horus run # automatically installs dependencies ``` ### "Failed to create Topic" Another program might be using the same topic name. Pick a unique name: ```python node.send("temperature_sensor_1", value) ``` ### Nothing prints Make sure both nodes are passed to `horus.run()`: ```python horus.run(make_sensor(), monitor) ``` ## What You've Learned - How to create a Python HORUS project with `horus new -p` - The functional `horus.Node(tick=fn)` pattern - Using `node.send()` and `node.recv()` for communication - Running multiple nodes with `horus.run()` - Sending and receiving messages in Python ## Ready for More? 1. **[Python API Reference](/python/api)** for the full API 2. **[Run the examples](/python/examples)** to see real applications 3. **[Open the monitor](/development/monitor)** to monitor your system visually For issues, see the **[Troubleshooting Guide](/troubleshooting)**. ## See Also - [Quick Start (Rust)](/getting-started/quick-start) - Build the same app in Rust - [Python API Reference](/python/api/python-bindings) - Full Python API documentation - [Python Examples](/python/examples) - Real Python applications - [Choosing a Language](/getting-started/choosing-language) - Rust vs Python comparison --- ## Quick Start Path: /getting-started/quick-start Description: Build your first HORUS application in 10 minutes # Quick Start > Prefer Python? See [Quick Start (Python)](/getting-started/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: 1. **Sensor** - Generates temperature readings 2. **Monitor** - Displays the readings They'll communicate using HORUS's ultra-fast shared memory. ## Step 1: Create a New Project ```bash # 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 (generated `Cargo.toml`, `target/`, packages/) > **Note**: `.horus/` is managed automatically — it contains the generated `Cargo.toml` and build artifacts. Your dependencies live in `horus.toml` and are pinned in `horus.lock`. See [Configuration Reference](/package-management/configuration) for details. ## Step 2: Write the Code Replace the generated `src/main.rs` with this complete example: ```rust use horus::prelude::*; use std::time::Duration; //=========================================== // SENSOR NODE - Generates temperature data //=========================================== struct TemperatureSensor { publisher: Topic, temperature: f32, } impl TemperatureSensor { fn new() -> Result { 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, } impl TemperatureMonitor { fn new() -> Result { 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! ```bash horus run --release ``` HORUS will automatically: - Read project config from `horus.toml` - Build with Cargo using your `Cargo.toml` dependencies - 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 ```rust // 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: ```rust 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: ```rust 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](/concepts/node-macro) for the full syntax. ### The Scheduler - Running Everything The scheduler runs your nodes in priority order: ```rust 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: ```bash # 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: ```bash horus run "*.rs" # All Rust files run as separate processes ``` > [TIP] **See [Topic](/concepts/core-concepts-topic#shared-memory-details)** for details on the shared memory architecture. ## Next Steps ### Add More Features Try modifying the code: **1. Add a temperature alert:** ```rust 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:** ```rust // In main(): scheduler.add(HumiditySensor::new()?) .order(0) .build()?; scheduler.add(HumidityMonitor::new()?) .order(1) .build()?; ``` **3. Save data to a file:** ```rust 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](/concepts/core-concepts-nodes)** - Deep dive into the Node pattern - **[Topic](/concepts/core-concepts-topic)** - How ultra-fast communication works - **[Scheduler](/concepts/core-concepts-scheduler)** - Priority-based execution **Make Development Easier:** - **[node! Macro](/concepts/node-macro)** - Eliminate boilerplate code - **[CLI Reference](/development/cli-reference)** - All the `horus` commands - **[Monitor](/development/monitor)** - Monitor your application visually **See More Examples:** - **[Examples](/rust/examples/basic-examples)** - Real applications you can run - **[Multi-Language](/concepts/multi-language)** - Use Python instead ## Common Questions ### Do I need `Box::new()`? No! The fluent API handles everything automatically: ```rust 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: ```rust Topic::new("temperature_sensor_1")? ``` ### "Address already in use" A shared memory region exists from a previous run. Clean it up: ```bash horus clean --shm ``` Or use a different topic name. ### Nothing prints Make sure both nodes are added: ```rust 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: 1. **[Use the node! macro](/concepts/node-macro)** to eliminate boilerplate 2. **[Run the examples](/rust/examples/basic-examples)** to see real applications 3. **[Open the monitor](/development/monitor)** to monitor your system For issues, see the **[Troubleshooting Guide](/troubleshooting)**. ## See Also - [Quick Start (Python)](/getting-started/quick-start-python) - Build the same app in Python - [Second Application](/getting-started/second-application) - Build a more advanced project - [Choosing a Language](/getting-started/choosing-language) - Rust vs Python comparison - [Tutorials](/tutorials) - Guided walkthroughs for common tasks --- ## Choosing a Language Path: /getting-started/choosing-language Description: Should you use Rust or Python with HORUS? # Choosing a Language HORUS supports both **Rust** and **Python**. This guide helps you choose the right one for your project. --- ## Quick Decision **Use Python if:** - You're prototyping or experimenting - You're new to robotics programming - You want to integrate with ML/AI libraries (TensorFlow, PyTorch) - Development speed matters more than runtime performance **Use Rust if:** - You need maximum performance - You're building production systems - You want compile-time safety guarantees - You're comfortable with Rust (or want to learn) --- ## Side-by-Side Comparison ### Hello World: Temperature Sensor **Python:** ```python import horus def sensor_tick(node): temp = 25.0 # Read sensor node.send("temperature", temp) sensor = horus.Node(name="TempSensor", tick=sensor_tick, order=5, pubs=["temperature"]) horus.run(sensor) ``` **Rust:** ```rust use horus::prelude::*; struct TempSensor { pub_topic: Topic, } impl TempSensor { fn new() -> Result { Ok(Self { pub_topic: Topic::new("temperature")? }) } } impl Node for TempSensor { fn name(&self) -> &str { "TempSensor" } fn tick(&mut self) { let temp = 25.0; // Read sensor self.pub_topic.send(temp); } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(TempSensor::new()?).order(5).build()?; scheduler.run() } ``` **Or with the Rust node! macro:** ```rust use horus::prelude::*; node! { TempSensor { pub { temperature: f32 -> "temperature" } tick { let temp = 25.0; self.temperature.send(temp); } } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(TempSensor::new()).order(5).build()?; scheduler.run() } ``` --- ## Detailed Comparison | Aspect | Python | Rust | |--------|--------|------| | **Learning curve** | Easy | Steeper | | **Setup time** | 5 minutes | 10 minutes | | **Compile time** | None | A few seconds | | **Runtime performance** | Good | Excellent | | **Memory safety** | Runtime checks | Compile-time guarantees | | **ML/AI integration** | Excellent (numpy, torch, etc.) | Limited | | **Debugging** | Simple print debugging | More tooling needed | | **Production readiness** | Good for prototypes | Production-grade | --- ## Performance Comparison | Operation | Python | Rust | Difference | |-----------|--------|------|------------| | Node tick latency | ~10μs | ~1μs | 10x faster | | Message send | ~2μs | ~400ns | 5x faster | | Control loop (1kHz) | Achievable | Easy | - | | Control loop (10kHz) | Difficult | Achievable | - | **Bottom line:** For most robotics applications, both are fast enough. Rust matters when you need very high-frequency control (>1kHz), hard real-time guarantees, or minimal memory footprint. ### Scheduling Features Both Rust and Python support the full scheduling API: | Feature | Rust | Python | Notes | |---------|------|--------|-------| | `.rate()` | Yes | Yes | Tick rate in Hz | | `.order()` | Yes | Yes | Execution priority | | `.budget()` | Yes | Yes | Tick time budget | | `.deadline()` | Yes | Yes | Hard deadline | | `.on_miss()` | Yes | Yes | Deadline miss policy | | `.priority()` | Yes | Yes | OS thread priority (SCHED_FIFO) | | `.core()` | Yes | Yes | CPU core pinning | | `.watchdog()` | Yes | Yes | Per-node watchdog timeout | | `.compute()` | Yes | Yes | CPU-bound thread pool | | `.async_io()` | Yes | Yes | I/O-bound async pool | | `.on(topic)` | Yes | Yes | Event-triggered execution | | `enter_safe_state()` | Yes | No | Requires implementing Node trait | **Performance difference:** While both languages expose the same API, Rust nodes achieve lower tick jitter and more predictable timing due to the absence of the GIL. For control loops above 1kHz with hard deadlines, Rust is recommended. **Best practice:** Use Rust for RT-critical driver nodes (motors, safety). Use Python for application logic (planning, ML inference, behavior trees). They communicate via shared memory topics — zero overhead across the language boundary. --- ## When to Choose Python ### Rapid Prototyping ```python # Quick experiment - try different approaches fast import horus def controller_tick(node): input_val = node.recv("sensor") or 0.0 strategy = "aggressive" if strategy == "aggressive": output = input_val * 2.0 else: output = input_val * 0.5 node.send("output", output) ctrl = horus.Node(name="ExperimentalController", tick=controller_tick, subs=["sensor"], pubs=["output"]) ``` ### Machine Learning Integration ```python import torch import horus model = torch.load("my_model.pt") def ml_tick(node): sensor_data = node.recv("sensor_data") if sensor_data is not None: with torch.no_grad(): output = model(torch.tensor(sensor_data)) node.send("control_output", output.item()) ml_node = horus.Node(name="MLController", tick=ml_tick, subs=["sensor_data"], pubs=["control_output"]) ``` ### Education and Learning Python's readable syntax makes it easier to understand robotics concepts without fighting the language. --- ## When to Choose Rust ### Production Deployments ```rust // Rust catches bugs at compile time impl Node for SafetyMonitor { fn tick(&mut self) { // Compiler ensures we handle all cases match self.check_safety() { SafetyStatus::OK => self.continue_operation(), SafetyStatus::Warning(msg) => self.log_warning(&msg), SafetyStatus::Critical(msg) => self.emergency_stop(&msg), } } } ``` ### High-Frequency Control ```rust // Rust can sustain 10kHz+ control loops impl Node for MotorController { fn tick(&mut self) { // Microsecond-level timing is reliable let error = self.target - self.position; let output = self.pid.compute(error); self.motor.send(output); } } ``` ### Resource-Constrained Environments ```rust // Rust has minimal runtime overhead // Perfect for embedded systems and single-board computers ``` --- ## Mixed Language Projects You can use both languages in the same project! HORUS nodes communicate via shared memory, which works across languages. **Example:** Python for AI, Rust for control (ML Inference)
10 Hz"] CTRL["Rust Node
(Controller)
100 Hz"] MOTOR["Rust Node
(Motor Driver)
1000 Hz"] PY --> CTRL --> MOTOR `} caption="Mixed language project: Python for ML, Rust for control - connected via shared memory" /> **Python ML node:** ```python import horus def detector_tick(node): camera_image = node.recv("camera") if camera_image is not None: detections = model.detect(camera_image) node.send("detections", detections) detector = horus.Node(name="ObjectDetector", tick=detector_tick, subs=["camera"], pubs=["detections"]) horus.run(detector) ``` **Rust control node:** ```rust impl Node for NavigationController { fn tick(&mut self) { if let Some(detections) = self.detection_sub.recv() { // React to Python node's output self.plan_path(&detections); } } } ``` --- ## Recommendation by Use Case | Use Case | Recommended Language | |----------|---------------------| | Learning HORUS | Python | | University project | Python | | Hobby robot | Either | | Machine learning robot | Python + Rust | | Industrial automation | Rust | | Drone/UAV | Rust | | Research prototype | Python | | Competition robot | Rust | | Product development | Rust | --- ## Getting Started **Ready to start with Python?** - [Quick Start (Python)](/getting-started/quick-start-python) - [Python API Reference](/python/api/python-bindings) **Ready to start with Rust?** - [Quick Start](/getting-started/quick-start) (uses Rust) - [node! Macro Guide](/concepts/node-macro) - [Rust API Reference](/rust/api) --- ## Still Unsure? **Start with Python.** It's faster to get something working, and you can always port critical parts to Rust later. HORUS makes it easy to mix languages. --- ## Common Mistakes Path: /getting-started/common-mistakes Description: Avoid these common beginner pitfalls when using HORUS # Common Mistakes New to HORUS? Here are the most common mistakes beginners make and how to fix them. --- ## 1. Using Slashes in Topic Names **The Problem:** ```rust // Works on Linux only — fails on macOS! let topic: Topic = Topic::new("sensors/lidar")?; ``` **Why:** On Linux, slashes create subdirectories in the shared memory filesystem which works fine. On macOS, `shm_open()` does not support slashes in names, so this will fail. **The Fix:** ```rust // CORRECT — Use dots for cross-platform compatibility let topic: Topic = Topic::new("sensors.lidar")?; ``` Use dot-separated names (`"sensors.lidar"`, `"camera.rgb"`) for portable topic names that work on all platforms. --- ## 2. Forgetting to Call recv() Every Tick **The Problem:** ```rust fn tick(&mut self) { // Only check for messages sometimes if self.counter % 10 == 0 { if let Some(data) = self.sensor_sub.recv() { self.process(data); } } self.counter += 1; } ``` **Why:** Messages can be missed if you don't check every tick. Topic uses a ring buffer (16-1024 slots by default), and old messages are overwritten when the buffer fills up. **The Fix:** ```rust fn tick(&mut self) { // ALWAYS check for new messages if let Some(data) = self.sensor_sub.recv() { self.last_data = Some(data); } // Use cached data for processing if self.counter % 10 == 0 { if let Some(ref data) = self.last_data { self.process(data); } } self.counter += 1; } ``` --- ## 3. Blocking in tick() **The Problem:** ```rust fn tick(&mut self) { // WRONG - This blocks the entire scheduler! let data = std::fs::read_to_string("large_file.txt").unwrap(); std::thread::sleep(Duration::from_millis(100)); } ``` **Why:** All nodes run in a single tick cycle. Blocking one node blocks them all. **The Fix:** ```rust fn init(&mut self) -> Result<()> { // Do slow initialization in init(), not tick() self.data = std::fs::read_to_string("large_file.txt")?; Ok(()) } fn tick(&mut self) { // Keep tick() fast - ideally under 1ms self.process(&self.data); } ``` --- ## 4. Wrong Priority Order **The Problem:** ```rust // WRONG - Logger runs before sensor! scheduler.add(logger).order(0).build()?; // Order 0 (runs first) scheduler.add(sensor).order(10).build()?; // Order 10 scheduler.add(controller).order(5).build()?; // Order 5 ``` **Why:** Lower order number = runs first. Safety-critical code should be order 0. **The Fix:** ```rust // CORRECT - Proper ordering scheduler.add(safety_monitor).order(0).build()?; // Safety first! scheduler.add(sensor).order(5).build()?; // Then sensors scheduler.add(controller).order(10).build()?; // Then control scheduler.add(logger).order(100).build()?; // Logging last ``` --- ## 5. Not Implementing shutdown() for Motors **The Problem:** ```rust impl Node for MotorController { fn name(&self) -> &str { "motor" } fn tick(&mut self) { self.motor.set_velocity(self.velocity); } // No shutdown() implemented! } ``` **Why:** When you press Ctrl+C, the motor keeps running at its last velocity! **The Fix:** ```rust impl Node for MotorController { fn name(&self) -> &str { "motor" } fn tick(&mut self) { self.motor.set_velocity(self.velocity); } fn shutdown(&mut self) -> Result<()> { // CRITICAL: Stop motor on shutdown! hlog!(info, "Stopping motor for safe shutdown"); self.motor.set_velocity(0.0); Ok(()) } } ``` --- ## 6. Not Deriving Required Traits for Custom Messages **The Problem:** ```rust struct MyMessage { x: f32, y: f32, } // Error: the trait bound `MyMessage: Clone` is not satisfied let topic: Topic = Topic::new("data")?; ``` **Why:** Topic requires types to implement `Clone + Send + Sync + Serialize + Deserialize`. Most Rust types are `Send + Sync` automatically, so you usually only need to derive the other three. **The Fix:** ```rust use serde::{Serialize, Deserialize}; #[derive(Clone, Serialize, Deserialize)] struct MyMessage { x: f32, y: f32, name: String, // Strings work fine! data: Vec, // Vecs work too! } let topic: Topic = Topic::new("data")?; ``` Or use the standard message types which already have the required traits: ```rust use horus::prelude::*; let topic: Topic = Topic::new("cmd_vel")?; let topic: Topic = Topic::new("odom")?; ``` --- ## 7. Thinking send() Returns a Result **The Problem:** ```rust fn tick(&mut self) { // WRONG - send() is infallible, this won't compile if let Err(e) = self.pub_topic.send(data) { hlog!(warn, "Failed to publish: {:?}", e); } } ``` **Why:** `send()` returns `()`, not `Result`. It uses ring buffer "keep last" semantics — when the buffer is full, the oldest message is overwritten. This means `send()` always succeeds. **The Fix:** ```rust fn tick(&mut self) { // CORRECT - send() is infallible, just call it self.pub_topic.send(data); } ``` --- ## 8. Creating Topic Inside tick() **The Problem:** ```rust fn tick(&mut self) { // WRONG - Creates new Topic every tick! let topic: Topic = Topic::new("data").unwrap(); topic.send(42.0); } ``` **Why:** Creating a Topic is expensive (opens shared memory). Doing it every tick wastes resources. **The Fix:** ```rust struct MyNode { topic: Topic, // Store Topic in struct } impl MyNode { fn new() -> Result { Ok(Self { topic: Topic::new("data")?, // Create once }) } } fn tick(&mut self) { self.topic.send(42.0); // Reuse existing Topic } ``` --- ## 9. Mismatched Topic Types **The Problem:** ```rust // Publisher sends f32 let pub_topic: Topic = Topic::new("data")?; pub_topic.send(42.0); // Subscriber expects i32 let sub_topic: Topic = Topic::new("data")?; // WRONG TYPE! let value = sub_topic.recv(); // Will get garbage data ``` **Why:** HORUS doesn't check types at runtime. Mismatched types cause data corruption. **The Fix:** ```rust // Use the SAME type for publisher and subscriber let pub_topic: Topic = Topic::new("data")?; let sub_topic: Topic = Topic::new("data")?; // Same type! ``` **Pro tip:** Use named message types to avoid confusion: ```rust type SensorReading = f32; let pub_topic: Topic = Topic::new("sensor")?; let sub_topic: Topic = Topic::new("sensor")?; ``` --- ## 10. Using Raw Node Trait When node! Macro Would Be Simpler **The Problem:** ```rust // Manual implementation - lots of boilerplate struct MySensor { pub_topic: Topic, } impl MySensor { fn new() -> Result { Ok(Self { pub_topic: Topic::new("sensor.data")?, }) } } impl Node for MySensor { fn name(&self) -> &str { "MySensor" } fn tick(&mut self) { let data = 42.0; // Read sensor self.pub_topic.send(data); } } ``` **The Fix:** ```rust // Use node! macro - 75% less code! node! { MySensor { pub { sensor_data: f32 -> "sensor.data" } tick { let data = 42.0; // Read sensor self.sensor_data.send(data); } } } ``` See [node! Macro](/concepts/node-macro) for more examples. --- ## Quick Reference | Mistake | Fix | |---------|-----| | Slashes in topic names | Use dots: `sensors.lidar` | | Not checking recv() every tick | Always call recv(), cache last value | | Blocking in tick() | Keep tick() under 1ms, do I/O in init() | | Wrong priority order | Lower number = higher priority | | No shutdown() for motors | Always stop actuators in shutdown() | | Missing derives on messages | Add Clone, Serialize, Deserialize | | Treating send() as fallible | `send()` is infallible — just call it directly | | Creating Topic in tick() | Create Topic once in new() | | Mismatched topic types | Use same type for pub and sub | | Too much boilerplate | Use the `node!` macro | --- ## Still Having Issues? - Check [Troubleshooting](/troubleshooting) for error messages - See [Examples](/rust/examples/basic-examples) for working code - Run `horus monitor` to see what your nodes are doing --- ## Building Your Second Application Path: /getting-started/second-application Description: Build a 3-node sensor pipeline with filtering and display # Building Your Second Application Now that you've built your first HORUS application, let's create something more practical: a **3-node sensor pipeline** that reads temperature data, filters out noise, and displays the results. ## What You'll Build A real-time temperature monitoring system with: 1. **SensorNode**: Publishes simulated temperature readings every second 2. **FilterNode**: Subscribes to raw temperatures, filters noise, republishes clean data 3. **DisplayNode**: Subscribes to filtered data, displays to console This demonstrates: - Multi-node communication patterns - Data pipeline processing - Real-time filtering - Monitor monitoring ## Architecture SensorNode
Publish 25.3°C"] -->|raw_temp| F["FilterNode
Remove noise"] F -->|filtered_temp| D["DisplayNode
Temp: 25.0°C"] style S fill:#3b82f6,color:#fff style F fill:#10b981,color:#fff style D fill:#f59e0b,color:#fff `} caption="Temperature pipeline: SensorNode → FilterNode → DisplayNode" /> ## Step 1: Create the Project ```bash horus new temperature_pipeline cd temperature_pipeline ``` ## Step 2: Write the Code Replace `src/main.rs` with this complete, runnable code: ```rust use horus::prelude::*; use std::time::{Duration, Instant}; // ============================================================================ // Node 1: SensorNode - Publishes temperature readings // ============================================================================ struct SensorNode { temp_pub: Topic, last_publish: Instant, reading: f32, } impl SensorNode { fn new() -> Result { Ok(Self { temp_pub: Topic::new("raw_temp")?, last_publish: Instant::now(), reading: 20.0, }) } } impl Node for SensorNode { fn name(&self) -> &str { "SensorNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Temperature sensor initialized"); Ok(()) } fn tick(&mut self) { // Publish every 1 second if self.last_publish.elapsed() >= Duration::from_secs(1) { // Simulate realistic temperature with noise // Base temperature oscillates between 20-30°C let base_temp = 25.0 + (self.reading * 0.1).sin() * 5.0; // Add random noise (+/- 2°C) let noise = (self.reading * 0.7).sin() * 2.0; let temperature = base_temp + noise; // Publish raw temperature self.temp_pub.send(temperature); hlog!(info, "Published raw temp: {:.2}°C", temperature); self.reading += 1.0; self.last_publish = Instant::now(); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Sensor shutdown complete"); Ok(()) } } // ============================================================================ // Node 2: FilterNode - Removes noise with exponential moving average // ============================================================================ struct FilterNode { raw_sub: Topic, filtered_pub: Topic, filtered_value: Option, alpha: f32, // Smoothing factor (0.0 - 1.0) } impl FilterNode { fn new() -> Result { Ok(Self { raw_sub: Topic::new("raw_temp")?, filtered_pub: Topic::new("filtered_temp")?, filtered_value: None, alpha: 0.3, // 30% new data, 70% previous (smooth but responsive) }) } } impl Node for FilterNode { fn name(&self) -> &str { "FilterNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Filter initialized (alpha = {:.2})", self.alpha); Ok(()) } fn tick(&mut self) { // Check for new temperature reading if let Some(raw_temp) = self.raw_sub.recv() { // Apply exponential moving average filter let filtered = match self.filtered_value { Some(prev) => self.alpha * raw_temp + (1.0 - self.alpha) * prev, None => raw_temp, // First reading, no previous value }; self.filtered_value = Some(filtered); // Publish filtered temperature self.filtered_pub.send(filtered); hlog!(info, "Filtered: {:.2}°C -> {:.2}°C (removed {:.2}°C noise)", raw_temp, filtered, raw_temp - filtered); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Filter shutdown complete"); Ok(()) } } // ============================================================================ // Node 3: DisplayNode - Shows filtered temperature on console // ============================================================================ struct DisplayNode { filtered_sub: Topic, display_counter: u32, } impl DisplayNode { fn new() -> Result { Ok(Self { filtered_sub: Topic::new("filtered_temp")?, display_counter: 0, }) } } impl Node for DisplayNode { fn name(&self) -> &str { "DisplayNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Display initialized"); println!("\n========================================"); println!(" Temperature Monitor - Press Ctrl+C to stop"); println!("========================================\n"); Ok(()) } fn tick(&mut self) { if let Some(temp) = self.filtered_sub.recv() { self.display_counter += 1; // Display temperature with visual indicator let status = if temp < 22.0 { "COLD" } else if temp > 28.0 { "HOT" } else { "NORMAL" }; println!( "[Reading #{}] Temperature: {:.1}°C - Status: {}", self.display_counter, temp, status ); hlog!(debug, "Displayed reading #{}", self.display_counter); } } fn shutdown(&mut self) -> Result<()> { println!("\n========================================"); println!(" Total readings displayed: {}", self.display_counter); println!("========================================\n"); hlog!(info, "Display shutdown complete"); Ok(()) } } // ============================================================================ // Main Application - Configure and run the scheduler // ============================================================================ fn main() -> Result<()> { println!("Starting Temperature Pipeline...\n"); let mut scheduler = Scheduler::new(); // Add nodes in priority order: // 1. SensorNode (order 0) - Runs first to generate data scheduler.add(SensorNode::new()?).order(0).build()?; // 2. FilterNode (order 1) - Runs second to process data scheduler.add(FilterNode::new()?).order(1).build()?; // 3. DisplayNode (order 2) - Runs last to display results scheduler.add(DisplayNode::new()?).order(2).build()?; println!("All nodes initialized. Running...\n"); // Run the scheduler (blocks until Ctrl+C) scheduler.run()?; Ok(()) } ``` ## Step 3: Run the Application ```bash horus run ``` **Expected Output:** ``` Starting Temperature Pipeline... All nodes initialized. Running... ======================================== Temperature Monitor - Press Ctrl+C to stop ======================================== [Reading #1] Temperature: 23.4°C - Status: NORMAL [Reading #2] Temperature: 24.1°C - Status: NORMAL [Reading #3] Temperature: 25.8°C - Status: NORMAL [Reading #4] Temperature: 27.2°C - Status: NORMAL [Reading #5] Temperature: 28.6°C - Status: HOT [Reading #6] Temperature: 27.9°C - Status: NORMAL [Reading #7] Temperature: 26.3°C - Status: NORMAL ``` Press **Ctrl+C** to stop: ``` ^C Ctrl+C received! Shutting down HORUS scheduler... ======================================== Total readings displayed: 7 ======================================== ``` ## Step 4: Monitor with Monitor Open a **second terminal** and run: ```bash horus monitor ``` The monitor will show: ### Nodes Tab - **SensorNode**: Publishing to `raw_temp` every ~1 second - **FilterNode**: Subscribing to `raw_temp`, publishing to `filtered_temp` - **DisplayNode**: Subscribing to `filtered_temp` ### Topics Tab - **raw_temp** (f32): Noisy temperature readings - **filtered_temp** (f32): Smooth temperature readings ### Metrics Tab - **IPC Latency**: ~85ns-437ns (sub-microsecond!) - **Tick Duration**: How long each node takes to execute - **Message Counts**: Total messages sent/received ## Understanding the Code ### SensorNode ```rust // Publish every 1 second if self.last_publish.elapsed() >= Duration::from_secs(1) { let temperature = 25.0 + noise; self.temp_pub.send(temperature); } ``` **Key Points:** - Uses `Instant` to track time between publishes - Simulates realistic sensor data with noise - Publishes to `"raw_temp"` topic ### FilterNode ```rust // Exponential moving average filter let filtered = self.alpha * raw_temp + (1.0 - self.alpha) * prev; self.filtered_pub.send(filtered); ``` **Key Points:** - Subscribes to `"raw_temp"`, publishes to `"filtered_temp"` - Implements exponential moving average (EMA) filter - `alpha = 0.3` balances responsiveness vs smoothness **Filter Behavior:** - **High alpha (0.8)**: Fast response, less smoothing - **Low alpha (0.2)**: Slow response, more smoothing ### DisplayNode ```rust if let Some(temp) = self.filtered_sub.recv() { println!("[Reading #{}] Temperature: {:.1}°C", count, temp); } ``` **Key Points:** - Subscribes to `"filtered_temp"` - Only receives when new data is available - `recv()` returns `None` when no message (not an error!) ## Common Issues & Fixes ### Issue 1: No Output Displayed **Symptom:** ``` Starting Temperature Pipeline... All nodes initialized. Running... ======================================== Temperature Monitor - Press Ctrl+C to stop ======================================== [Nothing appears] ``` **Cause:** Topics not connecting (typo in topic names) **Fix:** - Check topic names match exactly: `"raw_temp"` and `"filtered_temp"` - Verify with monitor: `horus monitor` -> Topics tab - Ensure all nodes are running in same scheduler ### Issue 2: Too Much/Too Little Smoothing **Symptom:** Temperature changes too fast or too slow **Fix:** Adjust the `alpha` value in `FilterNode`: ```rust alpha: 0.3, // Current: moderate smoothing // Try these alternatives: alpha: 0.7, // More responsive, less smooth alpha: 0.1, // Very smooth, slower response ``` ### Issue 3: Monitor Shows No Nodes **Symptom:** Monitor is empty **Cause:** Application not running or monitor started before app **Fix:** 1. Start the application first: `horus run` 2. Then start monitor in separate terminal: `horus monitor` 3. Monitor auto-discovers running nodes ### Issue 4: Build Errors **Symptom:** ``` error[E0433]: failed to resolve: use of undeclared type `Topic` ``` **Fix:** - Ensure HORUS is installed: `horus --help` - Check import: `use horus::prelude::*;` - Run from project directory (where `horus.toml` is) ## Experiments to Try ### 1. Change Update Rate Make the sensor publish faster: ```rust // In SensorNode::tick() if self.last_publish.elapsed() >= Duration::from_millis(500) { // 2 Hz instead of 1 Hz ``` ### 2. Add Temperature Alerts Add to `DisplayNode`: ```rust if temp > 30.0 { println!(" WARNING: High temperature detected!"); } ``` ### 3. Log Data to File Add to `DisplayNode::tick()`: ```rust use std::fs::OpenOptions; use std::io::Write; let mut file = OpenOptions::new() .create(true) .append(true) .open("temperature_log.txt") .unwrap(); writeln!(file, "{:.1}", temp).ok(); ``` ### 4. Use Rate-Limited Logging In a real robot running at 1kHz, you don't want every tick flooding the log. Use `hlog_once!` for one-time events and `hlog_every!` for periodic status: ```rust fn tick(&mut self) { // Log once when sensor first connects hlog_once!(info, "Sensor online, streaming data"); // Log status every 5 seconds (not every tick) hlog_every!(5000, info, "Filter running — last value: {:.1}°C", self.filtered_value.unwrap_or(0.0)); // Log warnings at most once per second if let Some(temp) = self.raw_sub.recv() { if temp > 35.0 { hlog_every!(1000, warn, "High temperature: {:.1}°C", temp); } } } ``` ### 5. Add Multiple Sensors Create a second sensor node: ```rust // In main() scheduler.add(SensorNode::new()?).order(0).build()?; // Sensor 1 scheduler.add(SensorNode::new()?).order(0).build()?; // Sensor 2 ``` Both will publish to the same topic, and FilterNode will process both! ## Key Concepts Demonstrated **Pipeline Pattern**: Data flows through stages (Sensor -> Filter -> Display) **Pub/Sub Decoupling**: Nodes don't know about each other, only topics **Real-Time Processing**: Filtering happens as data arrives **Shared Memory IPC**: Sub-microsecond communication between nodes **Priority Scheduling**: Sensor runs before filter, filter before display ## Shorter Version: node! Macro The entire pipeline above can be written more concisely with the `node!` macro. Here's the same 3-node system in roughly half the code: ```rust use horus::prelude::*; use std::time::{Duration, Instant}; node! { SensorNode { pub { temp_pub: f32 -> "raw_temp" } data { last_publish: Instant = Instant::now(), reading: f32 = 20.0, } init { hlog!(info, "Temperature sensor initialized"); Ok(()) } tick { if self.last_publish.elapsed() >= Duration::from_secs(1) { let base_temp = 25.0 + (self.reading * 0.1).sin() * 5.0; let noise = (self.reading * 0.7).sin() * 2.0; self.temp_pub.send(base_temp + noise); self.reading += 1.0; self.last_publish = Instant::now(); } } } } node! { FilterNode { sub { raw_sub: f32 -> "raw_temp" } pub { filtered_pub: f32 -> "filtered_temp" } data { filtered_value: Option = None, alpha: f32 = 0.3, } tick { if let Some(raw) = self.raw_sub.recv() { let filtered = match self.filtered_value { Some(prev) => self.alpha * raw + (1.0 - self.alpha) * prev, None => raw, }; self.filtered_value = Some(filtered); self.filtered_pub.send(filtered); } } } } node! { DisplayNode { sub { filtered_sub: f32 -> "filtered_temp" } data { count: u32 = 0 } tick { if let Some(temp) = self.filtered_sub.recv() { self.count += 1; let status = if temp < 22.0 { "COLD" } else if temp > 28.0 { "HOT" } else { "NORMAL" }; println!("[#{}] {:.1}°C - {}", self.count, temp, status); } } } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new()).order(0).build()?; scheduler.add(FilterNode::new()).order(1).build()?; scheduler.add(DisplayNode::new()).order(2).build()?; scheduler.run()?; Ok(()) } ``` The `node!` macro generates the struct, constructor, and `Node` trait implementation. Both versions produce identical runtime behavior. See the [node! Macro Guide](/concepts/node-macro) for the full syntax. ## Next Steps Now that you've built a 3-node pipeline, try: 1. **[Testing](/development/testing)** - Learn how to unit test your nodes 2. **[Using Pre-Built Nodes](/package-management/using-prebuilt-nodes)** - Use library nodes instead of writing from scratch 3. **[node! Macro](/concepts/node-macro)** - Reduce boilerplate with macros 4. **[Message Types](/concepts/message-types)** - Use complex message types instead of primitives ## Full Code The complete code above is production-ready. To save it: 1. Copy the entire code block 2. Replace `src/main.rs` in your project 3. Run with `horus run` For additional examples, see [Basic Examples](/rust/examples/basic-examples). ======================================== # SECTION: Learn ======================================== --- ## HORUS vs ROS2: A Practical Comparison Path: /learn/vs-ros2 Description: Detailed comparison of HORUS and ROS2 for robotics development — performance, architecture, developer experience, and when to use each. 575x faster IPC, deterministic scheduling, single-file config. # HORUS vs ROS2 This page compares HORUS and ROS2 for robotics development. Both are frameworks for building robot software — but they make fundamentally different architectural choices. This guide helps you decide which fits your project. > Already using ROS2? See [Coming from ROS2](/learn/coming-from-ros2) for a migration guide with side-by-side code examples. --- ## Quick Summary | Aspect | HORUS | ROS2 | |--------|-------|------| | **IPC Latency** | 87 ns (shared memory) | 50-100 μs (DDS) | | **Speedup** | **575x faster** for small messages | Baseline | | **Architecture** | Tick-based, single-process | Callback-based, multi-process | | **Execution Order** | Deterministic (priority + order) | Non-deterministic (callback queue) | | **Real-Time** | Auto-detected from `.rate()` / `.budget()` | Manual DDS QoS configuration | | **Config Files** | 1 file (`horus.toml`) | 3+ files (package.xml, CMakeLists.txt, launch.py) | | **Languages** | Rust, Python | C++, Python | | **Ecosystem** | Growing (core framework + package registry) | Massive (thousands of packages, 15+ years) | | **Multi-Machine** | Not yet (single-machine focus) | Native (DDS network transport) | | **Visualization** | `horus monitor` (web + TUI) | RViz2, Foxglove, rqt | | **License** | Apache 2.0 | Apache 2.0 | --- ## Performance ### Message Passing Latency HORUS uses shared memory directly. ROS2 uses DDS middleware (FastDDS, CycloneDDS) which adds serialization, discovery, and network stack overhead — even for same-machine communication. | Message Type | Size | HORUS | ROS2 (FastDDS) | Speedup | |---|---|---|---|---| | CmdVel | 16 B | ~85 ns | ~50 μs | **588x** | | IMU | 304 B | ~400 ns | ~55 μs | **138x** | | Odometry | 736 B | ~600 ns | ~60 μs | **100x** | | LaserScan | 1.5 KB | ~900 ns | ~70 μs | **78x** | | PointCloud (1K pts) | 12 KB | ~12 μs | ~150 μs | **13x** | | PointCloud (10K pts) | 120 KB | ~360 μs | ~800 μs | **2.2x** | The speedup is most dramatic for small, frequent messages (CmdVel, IMU) — exactly the messages that matter for tight control loops. ### Throughput | Metric | HORUS | ROS2 | |---|---|---| | Small messages (16B) | 2.7M msg/s | ~20K msg/s | | IMU messages (304B) | 1.8M msg/s | ~18K msg/s | | Mixed workload | 1.5M msg/s | ~15K msg/s | ### Real-Time Metrics | Metric | HORUS | ROS2 | |---|---|---| | Timing jitter | ±10 μs | ±100-500 μs | | WCET overhead | <5 μs per node | ~50-200 μs per callback | | Deadline enforcement | Built-in (`.budget()`, `.deadline()`) | Manual (rmw QoS) | | Emergency stop | <100 μs | Application-dependent | --- ## Architecture ### HORUS: Tick-Based, Deterministic ``` Scheduler runs one tick cycle: → Node 0 (order=0, safety monitor) → Node 1 (order=1, sensor reader) → Node 2 (order=2, controller) → Node 3 (order=3, actuator) → repeat ``` Every tick executes nodes in a guaranteed order. Node 2 always sees Node 1's latest data. No race conditions, no callback scheduling surprises. ### ROS2: Callback-Based, Event-Driven ``` Executor spins: → timer callback fires (sensor) → subscription callback fires (controller) → timer callback fires (actuator) → order depends on event timing ``` Callbacks fire when events arrive. Execution order depends on timing, message arrival, and executor implementation. Two runs of the same code may execute callbacks in different orders. ### Why This Matters For a motor controller that reads IMU data and sends actuator commands: - **HORUS**: IMU node ticks first (order=0), controller ticks second (order=1), actuator ticks third (order=2). Every cycle, guaranteed. - **ROS2**: IMU callback might fire before or after the controller callback. Under load, callbacks can be delayed or reordered. For safety-critical systems (surgical robots, industrial cobots), deterministic execution order is not optional. --- ## Developer Experience ### Project Setup **HORUS** (1 file): ```toml # horus.toml — single source of truth [package] name = "my-robot" version = "0.1.0" [dependencies] serde = "1.0" nalgebra = "0.32" ``` **ROS2** (3+ files): ```xml my_robot ament_cmake rclcpp sensor_msgs ``` ```cmake # CMakeLists.txt cmake_minimum_required(VERSION 3.8) find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) ``` ```python # launch/robot.launch.py from launch import LaunchDescription from launch_ros.actions import Node ``` ### Node Definition **HORUS** (Rust): ```rust struct MotorController { commands: Topic, } impl Node for MotorController { fn tick(&mut self) { if let Some(cmd) = self.commands.recv() { set_motor_velocity(cmd); } } } ``` **ROS2** (C++): ```cpp class MotorController : public rclcpp::Node { rclcpp::Subscription::SharedPtr sub_; void callback(std_msgs::msg::Float32::SharedPtr msg) { set_motor_velocity(msg->data); } public: MotorController() : Node("motor_controller") { sub_ = create_subscription( "commands", 10, std::bind(&MotorController::callback, this, std::placeholders::_1) ); } }; ``` HORUS: 10 lines. ROS2: 15 lines + shared pointers + bind + QoS depth parameter. ### CLI | Task | HORUS | ROS2 | |---|---|---| | Create project | `horus new my_robot` | `ros2 pkg create my_robot` + edit CMakeLists.txt | | Build | `horus build` | `colcon build` | | Run | `horus run` | `ros2 run my_robot my_node` | | List topics | `horus topic list` | `ros2 topic list` | | Echo topic | `horus topic echo velocity` | `ros2 topic echo /velocity` | | Monitor | `horus monitor` | `rqt` (separate install) | | Add dependency | `horus add serde` | Edit package.xml + CMakeLists.txt + rosdep install | --- ## When to Use HORUS **Choose HORUS when:** - You need sub-microsecond IPC latency (control loops >100Hz) - Deterministic execution order matters (safety-critical systems) - You want a single-file project config - Your robot runs on a single machine - You prefer Rust's safety guarantees - You want integrated monitoring and CLI tooling - You're starting a new project (no ROS2 migration debt) **Choose ROS2 when:** - You need multi-machine communication (distributed robots, fleet management) - You need RViz2 for 3D visualization - You depend on specific ROS2 packages (MoveIt2, Nav2, SLAM Toolbox) - Your team already knows ROS2 - You need the larger ecosystem (drivers, integrations, community support) - You need enterprise support options **Use both:** - HORUS for real-time control on the robot (sensors, actuators, safety) - ROS2 for high-level planning, visualization, fleet management on separate machines --- ## Feature Comparison | Feature | HORUS | ROS2 | |---|---|---| | **Pub/Sub Topics** | Yes (shared memory) | Yes (DDS) | | **Services (RPC)** | Beta | Yes | | **Actions (long-running)** | Beta | Yes | | **Parameters** | Yes (RuntimeParams) | Yes (Parameter Server) | | **Transform Frames** | Yes (TransformFrame) | Yes (tf2) | | **Recording/Replay** | Yes (Record/Replay) | Yes (rosbag2) | | **Monitoring** | Yes (web + TUI) | rqt, Foxglove (separate) | | **Launch System** | YAML launch files | Python/XML/YAML launch | | **Package Manager** | Yes (horus registry) | Yes (rosdep, bloom) | | **Deterministic Mode** | Yes (SimClock) | Partial (use_sim_time) | | **Safety Monitor** | Built-in (watchdog, degradation) | Application-level | | **Deadline Enforcement** | Built-in (`.budget()`, `.deadline()`) | Manual (rmw QoS) | | **Multi-Machine** | Not yet | Yes (DDS discovery) | | **3D Visualization** | Not yet | RViz2 | | **Simulation** | horus-sim3d (Bevy + Rapier) | Gazebo, Isaac Sim | | **Message IDL** | Rust structs (#[derive(Copy)]) | .msg/.srv/.action files | --- ## Migration Path HORUS and ROS2 can coexist. Common migration strategies: 1. **Start new subsystems in HORUS** — Keep existing ROS2 for high-level, add HORUS for real-time control 2. **Bridge approach** — Run a ROS2 bridge node that translates between DDS topics and HORUS topics 3. **Full migration** — Replace ROS2 nodes one-by-one with HORUS equivalents See [Coming from ROS2](/learn/coming-from-ros2) for detailed migration guidance with side-by-side code examples. --- ## Summary HORUS is **faster, simpler, and more deterministic** for single-machine robotics. ROS2 has a **larger ecosystem and multi-machine networking**. The right choice depends on your specific requirements — many production robots benefit from using both. --- ## Why HORUS? The Fastest Robotics Framework Path: /learn/why-horus Description: Why robotics teams choose HORUS over ROS2, DDS, and custom middleware. 575x faster IPC, deterministic scheduling, Rust safety, single-file config. Built for production robots. # Why HORUS? HORUS is a robotics framework built for teams that need **speed, determinism, and simplicity** — without the complexity of traditional middleware stacks. --- ## The Problem with Traditional Robotics Middleware Most robotics frameworks were designed in the 2000s-2010s for multi-machine, networked robot systems. They use serialization, network protocols, and discovery mechanisms that add overhead — even when all your components run on the same machine. **The result:** - 50-100 μs per message (DDS/ROS2) — fine for 10Hz planning, painful for 1kHz control - Non-deterministic callback ordering — debugging race conditions in safety-critical code - 3+ config files per package — CMakeLists.txt, package.xml, launch files, colcon workspace - Weeks of setup — DDS tuning, QoS profiles, executor configuration **Most robots don't need this.** A warehouse AGV, surgical robot, drone, or research platform runs all its control software on a single computer. The network layer is pure overhead. --- ## What HORUS Does Differently ### 1. Shared Memory IPC — 575x Faster HORUS topics use shared memory, not network protocols. No serialization, no copying, no middleware layer. | Message | HORUS | ROS2 (DDS) | Speedup | |---|---|---|---| | Motor command (16B) | 85 ns | 50 μs | **588x** | | IMU reading (304B) | 400 ns | 55 μs | **138x** | | LiDAR scan (1.5KB) | 900 ns | 70 μs | **78x** | | Point cloud (12KB) | 12 μs | 150 μs | **13x** | This isn't a synthetic benchmark — these are real robotics message types at real payload sizes. The speedup matters most for control loops above 100Hz where every microsecond counts. ### 2. Deterministic Execution — No Race Conditions HORUS runs nodes in a guaranteed order every tick: ```rust scheduler.add(SafetyMonitor::new()?).order(0).build()?; // Always first scheduler.add(SensorReader::new()?).order(1).build()?; // Always second scheduler.add(Controller::new()?).order(2).build()?; // Always third scheduler.add(Actuator::new()?).order(3).build()?; // Always last ``` No callback scheduling surprises. No mutex deadlocks. The safety monitor always runs before the actuator — every tick, guaranteed. ### 3. Auto-Detected Real-Time Set a rate or budget, and HORUS automatically enables RT features: ```rust scheduler.add(MotorNode::new()?) .order(0) .rate(1000.hz()) // 1kHz → auto-enables RT .budget(200.us()) // 200μs budget per tick .on_miss(Miss::Skip) // Skip tick if budget exceeded .build()?; ``` No DDS QoS tuning. No PREEMPT_RT kernel patches. No rmw configuration files. Just declare your timing requirements and HORUS handles the rest. ### 4. Single-File Config Everything in one `horus.toml`: ```toml [package] name = "warehouse-robot" version = "1.0.0" [dependencies] nalgebra = "0.32" opencv = { version = "0.92", source = "system" } [scripts] start = "horus run --release" test = "horus test --parallel" ``` No CMakeLists.txt. No package.xml. No colcon workspace. No launch files for simple cases. ### 5. Built-in Safety HORUS has safety features built into the scheduler — not bolted on: - **Watchdog**: Detects frozen nodes and triggers recovery - **Deadline enforcement**: `.budget()` and `.deadline()` are first-class - **Graduated degradation**: Ok → Warning → Expired → Critical (not binary crash/no-crash) - **Safe state**: Every node can implement `enter_safe_state()` (stop motors, close valves) - **Emergency stop**: <100 μs response time ### 6. Rust + Python — Your Choice ```rust // Rust: Maximum performance, compile-time safety struct Controller { cmd: Topic } impl Node for Controller { fn tick(&mut self) { self.cmd.send(compute_velocity()); } } ``` ```python # Python: Fast prototyping, ML integration def controller_tick(node): node.send("cmd", compute_velocity()) ``` Same framework, same topics, same scheduler. Mix Rust and Python nodes in the same application. Use Python for prototyping, Rust for production — or both simultaneously. --- ## Who Uses HORUS HORUS is designed for: - **Research labs** prototyping new robot behaviors (Python quick iteration) - **Startups** building production robots (Rust safety + performance) - **Control engineers** who need deterministic timing (auto-RT, deadline enforcement) - **Teams migrating from ROS2** who want simpler tooling without sacrificing capability - **Solo developers** who want to build a robot without weeks of framework setup --- ## What HORUS Is NOT (Yet) Being honest about current limitations: - **Not multi-machine** — HORUS is single-machine. For fleet management or distributed robots, use ROS2 for the network layer and HORUS for on-robot control - **No RViz equivalent** — `horus monitor` shows nodes, topics, and metrics, but not 3D visualization - **Smaller ecosystem** — ROS2 has thousands of packages. HORUS has a growing registry but can't match 15 years of community contributions - **No ROS2 bag compatibility** — HORUS has its own recording format These are active development areas. The core framework is production-ready; the ecosystem is growing. --- ## Get Started Ready to try HORUS? - **[Installation](/getting-started/installation)** — 5 minutes to install - **[Quick Start](/getting-started/quick-start)** — 10 minutes to your first robot - **[HORUS vs ROS2](/learn/vs-ros2)** — Detailed technical comparison - **[Benchmarks](/performance/benchmarks)** — Full performance data ```bash # Install and build your first robot in under 15 minutes curl -fsSL https://horusrobotics.dev/install.sh | sh horus new my-robot cd my-robot && horus run ``` --- ## Coming from ROS2 Path: /learn/coming-from-ros2 Description: A migration guide for ROS2 developers — concept mapping, architecture differences, and side-by-side code comparisons # Coming from ROS2 If you have experience with ROS2, you already know most of the concepts in HORUS. This guide maps what you know to how HORUS does it, highlights the architectural differences, and shows code side-by-side. ## Concept Mapping | ROS2 | HORUS | Notes | |------|-------|-------| | Node | `Node` trait | Same concept. Implement `tick()` instead of callbacks | | Publisher / Subscriber | `Topic` (send/recv) | Named channels, zero-copy via SHM | | Service | `Service` | Request/response, same pattern | | Action | `Action` | Long-running tasks with feedback | | tf2 | `TransformFrame` | `tf` / `tf_static` topics, tree lookups | | Parameter Server | `RuntimeParams` | Per-node typed parameters | | Launch file | `Scheduler` | Single process, all nodes in one scheduler | | rqt / Foxglove | `Monitor` | Built-in web dashboard + TUI | | rosbag | `Record` / `Replay` | Topic recording and playback | | QoS profiles | — | Not yet available | | Lifecycle node | `Node` trait | `init()` / `shutdown()` methods on every node | | DDS middleware | SHM IPC | No middleware layer, sub-microsecond latency | | `colcon build` | `horus build` | Single manifest (`horus.toml`), no CMake | | `ros2 topic echo` | `horus topic echo` | Same idea, different CLI | ## Architecture Differences ### ROS2: Multi-Process, Callback-Based In ROS2, each node is typically its own OS process. Nodes communicate over DDS (a network middleware), and you write callbacks that fire when messages arrive. Launch files coordinate which processes to start. ``` ┌─────────┐ DDS ┌─────────┐ DDS ┌─────────┐ │ Node A │ ◄──────► │ Node B │ ◄──────► │ Node C │ │ (pid 1) │ │ (pid 2) │ │ (pid 3) │ └─────────┘ └─────────┘ └─────────┘ ``` ### HORUS: Single-Process, Tick-Based In HORUS, all nodes live in one process. The scheduler calls each node's `tick()` in a deterministic order every cycle. Nodes communicate through shared-memory topics with zero-copy reads. ``` ┌───────────────────────────────────────┐ │ Scheduler │ │ ┌─────────┬─────────┬─────────┐ │ │ │ Node A │ Node B │ Node C │ │ │ │ tick() │ tick() │ tick() │ │ │ └────┬────┴────┬────┴────┬────┘ │ │ │ SHM Topics │ │ │ ─────┴─────────┴─────────┴───── │ └───────────────────────────────────────┘ ``` ### Why Tick-Based Matters for Real-Time | Property | ROS2 Callbacks | HORUS Ticks | |----------|---------------|-------------| | Execution order | Non-deterministic | Deterministic (`.order()`) | | Timing jitter | Depends on DDS, OS scheduling | Bounded by scheduler budget | | Deadline enforcement | Manual (timers) | Built-in (`.deadline()`, `.on_miss()`) | | Thread safety | You manage mutexes | Single-threaded tick, no locks needed | | Latency | Microseconds to milliseconds (DDS) | Sub-microsecond (SHM) | ### Cross-Process Communication HORUS nodes can still talk across processes. SHM topics are visible to any process on the same machine. You simply run two schedulers that share the same topic names — no DDS required. ## Code Comparison Here is the same motor controller node in ROS2 C++ and HORUS Rust. ### ROS2 C++ ```cpp #include #include #include class MotorNode : public rclcpp::Node { public: MotorNode() : Node("motor") { sub_ = create_subscription( "imu", 10, [this](sensor_msgs::msg::Imu::SharedPtr msg) { last_imu_ = *msg; }); pub_ = create_publisher("cmd_vel", 10); timer_ = create_wall_timer(10ms, [this]() { tick(); }); } private: void tick() { geometry_msgs::msg::Twist cmd; cmd.linear.x = compute_speed(last_imu_); pub_->publish(cmd); } rclcpp::Subscription::SharedPtr sub_; rclcpp::Publisher::SharedPtr pub_; rclcpp::TimerBase::SharedPtr timer_; sensor_msgs::msg::Imu last_imu_; }; int main(int argc, char** argv) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared()); } ``` ### HORUS Rust ```rust use horus::prelude::*; struct MotorNode { imu_sub: Topic, cmd_pub: Topic, } impl MotorNode { fn new() -> Result { Ok(Self { imu_sub: Topic::new("imu")?, cmd_pub: Topic::new("cmd_vel")?, }) } } impl Node for MotorNode { fn name(&self) -> &str { "motor_node" } fn tick(&mut self) { if let Some(imu) = self.imu_sub.recv() { let cmd = Twist::default(); // compute from IMU self.cmd_pub.send(cmd); } } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(MotorNode::new()?) .order(0) .rate(100_u64.hz()) .build()?; scheduler.run() } ``` **Key differences:** - No callback boilerplate — `tick()` reads and writes directly - Rate is set on the scheduler, not via a timer - No `SharedPtr`, no mutex — the scheduler guarantees single-threaded access - `Scheduler::run()` replaces `rclcpp::spin()` ## Message Type Mapping | ROS2 Message | HORUS Type | Module | |-------------|-----------|--------| | `sensor_msgs/Imu` | `Imu` | `horus::messages` | | `sensor_msgs/LaserScan` | `LaserScan` | `horus::messages` | | `sensor_msgs/Image` | `Image` | `horus::memory` | | `sensor_msgs/JointState` | `JointState` | `horus::messages` | | `sensor_msgs/PointCloud2` | `PointCloud` | `horus::memory` | | `geometry_msgs/Twist` | `Twist` | `horus::messages` | | `geometry_msgs/Pose` | `Pose3D` | `horus::messages` | | `geometry_msgs/Transform` | `TFMessage` | `horus::transform_frame` | | `nav_msgs/Odometry` | `Odometry` | `horus::messages` | | `std_msgs/String` | `String` | Rust stdlib | | `std_msgs/Bool` | `bool` | Rust stdlib | | `std_msgs/Float64` | `f64` | Rust stdlib | ## What HORUS Adds Over ROS2 **Zero-copy SHM.** Topics use shared memory by default. Readers get a direct pointer to the data — no serialization, no copy. This gives sub-microsecond publish-to-read latency. **Deterministic mode.** The scheduler can run in lockstep with a simulation clock. Every tick produces identical results given the same inputs. This is critical for sim-to-real transfer. **Built-in safety monitor.** Every node has a watchdog. If a node exceeds its deadline, the scheduler can warn, skip the node, reduce its rate, or trigger a safe-state shutdown — all configured per-node via `.on_miss()`. **Auto-RT detection.** Set `.rate()` or `.budget()` on a node and HORUS automatically classifies it as real-time. No need to manually configure thread priorities or scheduling policies. **Single-file configuration.** One `horus.toml` replaces `package.xml`, `CMakeLists.txt`, `setup.py`, and launch files. Dependencies, scripts, and node configuration all live in one place. ## What HORUS Doesn't Have Yet **Multi-machine networking.** HORUS currently runs on a single machine. SHM topics do not cross network boundaries. For multi-machine setups, you would need a custom bridge. **Visualization (rviz equivalent).** There is no 3D visualization tool like rviz. The Monitor provides metrics dashboards but not scene rendering. **Bag file format.** Record/Replay works but uses an internal format. There is no equivalent to the rosbag2 format or interoperability with ROS2 bags. **QoS profiles.** There is no quality-of-service configuration for topics (reliability, durability, history depth). Topics are currently best-effort with configurable buffer sizes. **Ecosystem breadth.** ROS2 has thousands of community packages. HORUS is younger and has a smaller library of pre-built drivers and algorithms. Check the [HORUS Registry](https://registry.horusrobotics.dev) for available packages. ## Migration Checklist If you are porting a ROS2 project to HORUS: 1. **Map your nodes.** Each ROS2 node becomes a struct implementing the `Node` trait 2. **Replace callbacks with `tick()`.** Read all inputs at the top of `tick()`, compute, then publish outputs 3. **Convert message types.** Use the mapping table above. Custom messages become Rust structs 4. **Replace launch files.** Build your scheduler in `main()` with `.add()` calls 5. **Replace `package.xml` + `CMakeLists.txt`.** Write one `horus.toml` 6. **Replace tf2 with TransformFrame.** Same tree semantics, publish to `tf` / `tf_static` topics 7. **Test with `tick_once()`.** HORUS supports single-tick execution for deterministic unit tests --- ## Learn HORUS Path: /learn Description: Understand the core concepts behind HORUS — from nodes and topics to real-time control and safety # Learn HORUS This section explains **what HORUS is and why it works the way it does**. Read these pages to build a mental model before writing code. ## Start Here | If you're... | Read this first | |-------------|----------------| | New to robotics | [What is HORUS?](/concepts/what-is-horus) → [Architecture](/concepts/architecture) | | New to real-time systems | [Real-Time for Robotics](/concepts/real-time) | | Coming from ROS2 | [Coming from ROS2](/learn/coming-from-ros2) | | Ready to build | Jump to [Tutorials](/tutorials) | ## Concepts 1. **[What is HORUS?](/concepts/what-is-horus)** — The problem HORUS solves and how it's different from ROS2 2. **[Architecture](/concepts/architecture)** — Nodes, topics, scheduler — how the pieces fit together 3. **[Real-Time for Robotics](/concepts/real-time)** — What "real-time" means, why robots need it, and when you don't 4. **[Communication Patterns](/concepts/communication-overview)** — Topics vs services vs actions — when to use which 5. **[Safety & Fault Tolerance](/advanced/circuit-breaker)** — Watchdogs, failure policies, and safe states 6. **[Coordinate Transforms](/concepts/transform-frame)** — Frame chains, interpolation, and why robots need transforms 7. **[Coming from ROS2](/learn/coming-from-ros2)** — Concept mapping, architecture differences, and migration guide ======================================== # SECTION: Core Concepts ======================================== --- ## Nodes: The Building Blocks Path: /concepts/nodes-beginner Description: What HORUS nodes are and how they work — a 5-minute introduction # Nodes: The Building Blocks > For the full reference with all lifecycle methods, priority levels, and communication patterns, see [Nodes — Full Reference](/concepts/core-concepts-nodes). ## What is a Node? A **node** is one piece of your robot's software. Each node does **one job**: - A **SensorNode** reads the camera or IMU - A **ControlNode** moves the motors - A **SafetyNode** prevents collisions - A **PlannerNode** decides where to go Nodes are **independent** — if one crashes, the others keep running. This is critical for robots: a camera driver bug shouldn't stop your emergency brake. ## Your First Node Every node implements the `Node` trait. The only required method is `tick()` — your main logic that runs every cycle: ```rust use horus::prelude::*; struct Heartbeat; impl Node for Heartbeat { fn name(&self) -> &str { "Heartbeat" } fn tick(&mut self) { println!("Robot is alive!"); } } ``` That's it. The scheduler calls `tick()` repeatedly — you don't manage loops, threads, or timing. **In Python:** ```python import horus def heartbeat_tick(node): print("Robot is alive!") heartbeat = horus.Node(name="Heartbeat", tick=heartbeat_tick) ``` ## How Nodes Communicate Nodes don't call each other directly. They send data through **Topics** — named channels for specific data types: |send temperature| T(("temperature")) T -->|recv temperature| M["MonitorNode"] style T fill:#f59e0b,stroke:#d97706,color:#000 `} caption="Nodes communicate through Topics, not direct calls" /> The sensor doesn't know the monitor exists. It just publishes data. Any number of subscribers can listen — zero coupling between components. ## Node Lifecycle Every node has three phases: init: Scheduler starts init --> tick: init() returns Ok tick --> tick: Every cycle tick --> shutdown: Ctrl+C or error shutdown --> [*] `} caption="Node lifecycle: init once, tick repeatedly, shutdown once" /> | Phase | Method | When | Use For | |-------|--------|------|---------| | **Startup** | `init()` | Once, before first tick | Open files, connect to hardware, create topics | | **Running** | `tick()` | Every scheduler cycle | Read sensors, compute, send commands | | **Cleanup** | `shutdown()` | Once, on exit | Stop motors, close connections, save state | ```rust impl Node for MotorController { fn init(&mut self) -> Result<()> { self.motor.connect()?; // Open hardware connection Ok(()) } fn tick(&mut self) { if let Some(cmd) = self.commands.recv() { self.motor.set_velocity(cmd); // Move motor } } fn shutdown(&mut self) -> Result<()> { self.motor.set_velocity(0.0); // STOP the motor! self.motor.disconnect()?; Ok(()) } } ``` The shutdown method is especially important for robotics — you always want to stop motors and release hardware safely. ## Running a Node Nodes run inside a **Scheduler**. You create one, add your nodes, and run: ```rust fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new()?) .order(0) // Runs first .build()?; scheduler.add(ControlNode::new()?) .order(1) // Runs second .build()?; scheduler.run()?; // Runs until Ctrl+C Ok(()) } ``` The `order` parameter controls execution sequence: lower numbers run first. This is how you ensure the sensor reads data before the controller processes it. ## Key Takeaways - A **node** = one component doing one job - Implement `tick()` for your main logic - Use `init()` for setup, `shutdown()` for cleanup - Nodes communicate through **Topics**, not direct calls - The **Scheduler** runs your nodes in order ## Next Steps - [Topics: How Nodes Talk](/concepts/topics-beginner) — learn about the pub/sub system - [Scheduler: Running Your Nodes](/concepts/scheduler-beginner) — learn execution and timing - [Quick Start](/getting-started/quick-start) — build a complete working example - [Nodes — Full Reference](/concepts/core-concepts-nodes) — all lifecycle methods, priority levels, and patterns --- ## What is HORUS? Path: /concepts/what-is-horus Description: A high-performance framework for building distributed systems with sub-microsecond IPC # What is HORUS? HORUS is a framework for building applications with multiple independent components that communicate through ultra-low-latency shared memory. Each component handles one responsibility, and they connect together to form complex systems. ## The Core Idea Instead of writing one monolithic program, you build: - **Independent Nodes** - Each component is self-contained - **Connected by Topics** - Nodes communicate through named channels - **Run by a Scheduler** - HORUS manages execution order **Example:** A robot control system might have: - SensorNode (reads camera) - VisionNode (detects objects) - ControlNode (moves motors) - SafetyNode (prevents collisions) Each node runs independently, sharing data through topics like `"camera.image"`, `"detected.objects"`, `"motor.commands"`. ## Key Features ### 1. Low-Latency Communication HORUS achieves **~3ns to ~167ns** message latency through shared memory — up to **500x faster** than traditional middleware. The system automatically selects the optimal communication path based on your topology (same-thread, same-process, or cross-process). No configuration needed. This performance enables tight control loops and high-frequency data processing without introducing significant delay. ### 2. Clean APIs HORUS provides idiomatic Rust APIs: ```rust use horus::prelude::*; // Create a publisher let publisher: Topic = Topic::new("temperature")?; // Create a subscriber let subscriber: Topic = Topic::new("temperature")?; // Send data publisher.send(25.0); // Receive data if let Some(temp) = subscriber.recv() { println!("Temperature: {:.1}C", temp); } ``` **With the node macro:** ```rust node! { SensorNode { pub { temperature: f32 -> "temperature", } tick { self.temperature.send(25.0); } } } ``` ### 3. Built-in Developer Tools ```bash # Create a new project horus new my_project # Run your application horus run # Monitor in real-time horus monitor ``` The monitor displays: - Running nodes - Message flow between components - Performance metrics (latency, throughput) - Debugging information ### 4. Multi-Language Support **Python:** ```python import horus def sensor_tick(node): node.send("temperature", 25.0) sensor = horus.Node(name="sensor", tick=sensor_tick, order=0, pubs=["temperature"]) horus.run(sensor) ``` **Rust:** Full framework capabilities for performance-critical code. Python and Rust nodes can communicate in the same application. ## Core Concepts ### Nodes A **Node** is a component that performs a specific task. Examples: - Read data from a sensor - Process information - Control a motor - Display data on screen - Monitor system health **Node lifecycle:** 1. **init()** - Start up (optional, run once) 2. **tick()** - Do work (runs repeatedly) 3. **shutdown()** - Clean up (optional, run once) ```rust impl Node for MySensor { fn name(&self) -> &str { "MySensor" } fn init(&mut self) -> Result<()> { hlog!(info, "Sensor starting up"); Ok(()) } fn tick(&mut self) { // Read sensor, send data - runs repeatedly } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Sensor shutting down"); Ok(()) } } ``` ### Topics A **Topic** is a named channel for sending messages. Multiple publishers can send to a topic, and multiple subscribers can receive from it. **Topic naming conventions:** - Use descriptive names: `"temperature"`, `"camera.image"`, `"motor.speed"` - Use dots for hierarchy: `"sensors.imu.accel"`, `"actuators.left_wheel"` ```rust // Node A publishes temperature let pub_a: Topic = Topic::new("temperature")?; pub_a.send(25.0); // Node B also publishes temperature let pub_b: Topic = Topic::new("temperature")?; pub_b.send(30.0); // Node C receives from both let sub: Topic = Topic::new("temperature")?; if let Some(temp) = sub.recv() { println!("Got: {}", temp); } ``` **Type safety:** The type parameter (``) ensures publishers and subscribers use the same message type. The compiler prevents type mismatches at compile time. **Note:** HORUS supports both **typed messages** (Pose2D, CmdVel, etc.) and **generic messages** (dicts/JSON in Python). Typed messages provide better performance and type safety. See [Message Types](/concepts/message-types) for details. ### Scheduler The **Scheduler** runs nodes in priority order: ```rust let mut scheduler = Scheduler::new(); // Add nodes with order (lower number = runs first) scheduler.add(SensorNode::new()?).order(0).build()?; // Runs first scheduler.add(ProcessNode::new()?).order(1).build()?; // Runs second scheduler.add(DisplayNode::new()?).order(2).build()?; // Runs third // Run (Ctrl+C to stop) scheduler.run()?; ``` **Execution order:** 1. All order 0 nodes tick first 2. Then all order 1 nodes tick 3. Then all order 2 nodes tick 4. Repeat **Use order to control data flow:** - Sensors should run before processors (lower order number) - Processors should run before actuators - Safety checks should run first (order 0) ## When to Use HORUS ### Suitable Applications **Multi-component applications** - Isolated components that communicate: - Robot control systems - Real-time data processing pipelines - Multi-sensor fusion systems - Parallel processing workflows **Real-time systems** - When latency matters: - Control loops (motor control, flight control) - High-frequency data processing - Live audio/video processing **Single-machine distributed systems** - Multiple processes on one machine: - Embedded Linux systems (Raspberry Pi, Jetson) - Edge computing devices - Multi-core applications **Hardware integration** - Combining multiple devices/languages: - Mix Rust (performance) + Python (ease of use) - Integrate Python prototypes with production Rust ### Robotics Scenarios | Scenario | Why HORUS | Key Metric | |----------|-----------|------------| | **High-Speed Manipulation** (surgical, pick-and-place) | Sub-microsecond latency enables 1kHz+ control loops | ~85-500ns CmdVel latency | | **Drone Control** (quadcopters, racing drones) | Fast IMU processing for real-time attitude control | ~400-940ns for 304B IMU messages | | **Collaborative Robots** (cobots, force-torque control) | Low latency force feedback + memory safety | Priority-based safety scheduling | | **Autonomous Vehicles** (mobile robots, warehouse AGVs) | Fast laser scan processing for obstacle detection | ~900ns-2.2µs for 1.5KB scans | | **Industrial Automation** (production lines, machine vision) | Deterministic latency for 24/7 operation | Predictable sequential execution | | **Research Prototyping** (university labs, algorithm dev) | Simple API, fast iteration, prototype-to-production | Same code runs in dev and prod | | **Teleoperation & Haptics** (remote surgery, VR) | Ultra-low latency eliminates perceptible feedback lag | ~85-500ns end-to-end | | **Multi-Robot Systems** (swarms, fleets) | Registry enables code sharing across fleets | Isolated environments per robot type | ### Less Suitable Applications **Simple single-script programs** — If your program fits in 100 lines, HORUS adds unnecessary complexity. **Internet-scale distributed systems** — HORUS is optimized for single-machine shared memory IPC, not WAN/internet communication. For cross-machine communication, use gRPC, HTTP, or message queues. **CRUD web applications** — Use web frameworks (Axum, Actix, Django, Flask) instead. **Bare-metal embedded systems** — HORUS requires an operating system with shared memory support. For microcontrollers, use RTIC or Embassy. **Pure simulation** — Use standalone simulators for pure simulation needs. ## Comparison with Other Frameworks ### vs Monolithic Programs **Traditional approach:** ```rust fn main() { loop { let temp = read_sensor(); let filtered = process(temp); display(filtered); } } ``` **Issues:** - Difficult to test individual parts - Changes can break everything - Components cannot be reused - No parallelization **HORUS approach:** ```rust // SensorNode - reusable, testable, independent struct SensorNode { data_pub: Topic } // ProcessNode - can swap implementation struct ProcessNode { data_sub: Topic, processed_pub: Topic } // DisplayNode - can replace with LogNode, etc. struct DisplayNode { data_sub: Topic } ``` **Benefits:** - Test each node independently - Change one node without affecting others - Reuse nodes across projects - Nodes can run in parallel ### vs ROS (Robot Operating System) | Aspect | HORUS | ROS | |--------|-------|-----| | Typical latency | ~0.003-0.167 µs | 50-100 µs (intra-machine DDS) | | Configuration | Code-based | XML files | | Target use case | Single-machine performance | Multi-machine robotics | | Ecosystem | Growing | Large | **Use ROS when:** You need cross-machine communication or extensive robotics libraries. **Use HORUS when:** You need high performance on a single machine. ### vs Message Queues (RabbitMQ, Kafka) | Aspect | HORUS | Message Queues | |--------|-------|----------------| | Latency | ~0.003-0.167 µs | 1-10 ms | | Scope | Single machine | Multi-machine | | Configuration | Minimal | Complex | | Persistence | No | Yes | **Use message queues when:** You need multi-machine communication, persistence, or reliability guarantees. **Use HORUS when:** You need high speed on a single machine. ## Architecture Overview pub"] PN["Process Node
sub/pub"] DN["Display Node
sub"] end subgraph Topics["Shared Memory Topics"] T1["sensor.temp"] T2["filtered.temp"] end subgraph Framework["HORUS Framework"] F1["Topic — Pub/Sub Communication"] F2["Scheduler — Priority Execution"] F3["Logging — Debugging & Metrics"] F4["Monitor — Real-time Monitoring"] end subgraph OS["Operating System
Linux / macOS / Windows"] SHM["Platform Shared Memory"] end SN --> T1 T1 --> PN PN --> T2 T2 --> DN Framework <--> OS `} caption="HORUS architecture overview" /> **Data flow:** 1. Nodes communicate via Topics 2. Topics write/read from shared memory 3. Scheduler orchestrates node execution 4. Monitor displays system status in real-time ## Technical Details ### Performance Characteristics **IPC Latency:** - Same-thread: ~3ns - Same-process: ~18-36ns - Cross-process: ~50-167ns **Throughput:** - Small messages (<1KB): 2M+ msgs/sec - Large messages (1MB): Limited by memory bandwidth **Memory Usage:** - Framework overhead: ~2MB - Per topic: Auto-sized based on message type (~73KB to ~8MB) - Per node: Depends on implementation ### Built in Rust HORUS leverages Rust for: **Safety** - Compile-time guarantees: - No null pointer dereferences - No data races - No use-after-free bugs **Performance** - Zero-cost abstractions: - No garbage collection pauses - Predictable memory layout - LLVM optimizations **Concurrency** - Fearless concurrency: - Send/Sync traits prevent data races - Ownership prevents sharing mutable state ## Learning Path **Start here:** 1. [Installation](/getting-started/installation) - Get HORUS installed 2. [Quick Start](/getting-started/quick-start) - Build your first app 3. [Basic Examples](/rust/examples/basic-examples) - Working examples **Core concepts:** 4. [Nodes](/concepts/core-concepts-nodes) - Build components 5. [Topic](/concepts/core-concepts-topic) - Pub/sub communication 6. [Scheduler](/concepts/core-concepts-scheduler) - Run your application **Practical features:** 7. [node! Macro](/concepts/node-macro) - Reduce boilerplate 8. [Monitor](/development/monitor) - Monitor and debug **Advanced:** 9. [Multi-Language](/concepts/multi-language) - Python integration 10. [Performance](/performance/performance) - Optimization 11. [Examples](/rust/examples/basic-examples) - Real projects ## Next Steps 1. **[Install HORUS](/getting-started/installation)** - Get up and running 2. **[Quick Start Tutorial](/getting-started/quick-start)** - Build your first application 3. **[See Examples](/rust/examples/basic-examples)** - Learn from real projects For command reference, see [CLI Reference](/development/cli-reference). For architecture details, see [Architecture](/concepts/architecture). ## See Also - [Quick Start](/getting-started/quick-start) - Build your first HORUS application - [Why HORUS](/learn/why-horus) - Detailed motivation and design goals - [vs ROS2](/learn/vs-ros2) - Side-by-side comparison with ROS2 - [Architecture](/concepts/architecture) - System architecture and internals --- ## Real-Time Systems Path: /concepts/real-time Description: What real-time means for robotics, why it matters, and how Horus handles it # Real-Time Systems ## Key Takeaways After reading this guide, you will understand: - What "real-time" actually means (predictable, not fast) - Why robots need real-time guarantees for safety and stability - The difference between hard, soft, and firm real-time - Where Horus fits in the real-time stack - Which Horus features give you real-time behavior - When you do NOT need real-time at all ## What is Real-Time? Real-time does not mean "fast." It means **predictable**. A real-time system guarantees that work finishes within a **bounded time**, every single time. A 10ms deadline means every tick must complete in 10ms — not just the average, not 99% of them, but *every one*. ``` Regular system: "Process this as fast as you can" Real-time system: "Process this, and you MUST finish within 10ms" ``` A regular program that finishes in 1ms on average but occasionally takes 500ms is **faster** than a real-time program that always takes 9ms. But the real-time program is **more reliable** — and for robots, reliability beats speed. ## Why Robots Need Real-Time Three things break when timing is unpredictable: **Motor control loops.** A controller sends velocity commands at 100Hz. If one command arrives 50ms late, the motor runs at the old speed for 5x too long. The result: overshoot, oscillation, or a robot arm slamming into a table. **Sensor fusion.** An IMU reports acceleration at 200Hz. If the fusion algorithm processes a sample late, it integrates stale data and the pose estimate drifts. Late by 10ms at 1m/s means 1cm of position error — per missed sample. **Safety systems.** A watchdog must detect a frozen node and trigger a stop within a bounded time. If the watchdog itself is delayed by a garbage collection pause, the robot keeps moving when it should have stopped. ## Hard vs Soft vs Firm Real-Time | Type | If you miss a deadline... | Example | |------|--------------------------|---------| | **Hard** | System failure. People get hurt. | Pacemaker, ABS brakes, airbag | | **Firm** | Result is worthless, but system survives | Sensor fusion with stale data, dropped video frame | | **Soft** | Quality degrades gracefully | Video streaming, audio playback | **Horus is a soft real-time framework.** It runs in Linux userspace, so it cannot make hard real-time guarantees — the kernel can always preempt you. But it provides the tools to get consistent, low-latency execution that is good enough for most robotics applications. For hard real-time (sub-microsecond PWM, current loops, safety-critical interlocks), you need firmware or an RTOS running on a dedicated microcontroller. ## Where Horus Fits ``` ┌─────────────────────────────────────────────┐ │ Application Layer │ │ Planning, behavior trees, mission logic │ │ Timing: 1-10 Hz, best-effort │ ├─────────────────────────────────────────────┤ │ Horus (soft RT, Linux userspace) │ │ Perception, control loops, sensor fusion │ │ Timing: 50-1000 Hz, ms-level deadlines │ ├─────────────────────────────────────────────┤ │ Firmware / RTOS (hard RT) │ │ PWM generation, current loops, IMU sampling│ │ Timing: 1-50 kHz, μs-level deadlines │ ├─────────────────────────────────────────────┤ │ Hardware │ │ Motors, sensors, encoders, batteries │ └─────────────────────────────────────────────┘ ``` Horus sits in the middle. It is fast enough to close control loops at hundreds of Hz, but it delegates microsecond-level work to firmware. This is the right tradeoff for most robots — you get the full power of Linux (networking, file I/O, ML inference) while still meeting timing constraints. ## Horus RT Features Horus gives you six tools for real-time behavior. You can use as many or as few as you need. ### Auto-derived timing from `.rate()` Set a tick rate and Horus calculates safe defaults: ```rust scheduler.add(controller) .rate(100.hz()) // 10ms period → 8ms budget (80%), 9.5ms deadline (95%) .build()?; ``` This is the easiest way to get RT behavior. See [Scheduler](/concepts/core-concepts-scheduler) for details. ### Explicit `.budget()` and `.deadline()` For fine-grained control, set them directly: ```rust scheduler.add(controller) .rate(100.hz()) .budget(5.ms()) // Must finish compute in 5ms .deadline(8.ms()) // Must complete full cycle in 8ms .build()?; ``` If you set `.budget()` without `.deadline()`, the deadline is automatically set equal to the budget — your budget IS your hard deadline: ```rust scheduler.add(controller) .budget(500.us()) // budget=500μs, deadline=500μs (auto-derived) .on_miss(Miss::Stop) // Fires when tick exceeds 500μs .build()?; ``` ### `.on_miss()` — Deadline miss handling Tell Horus what to do when a tick takes too long: ```rust use horus::prelude::*; scheduler.add(controller) .rate(100.hz()) .on_miss(Miss::Warn) // Log a warning (default) .on_miss(Miss::Skip) // Drop this tick, move on .on_miss(Miss::SafeMode) // Enter safe state .on_miss(Miss::Stop) // Shut down the scheduler .build()?; ``` ### `.prefer_rt()` — OS-level scheduling Requests `SCHED_FIFO` from the Linux kernel, giving your process priority over all normal processes: ```rust let mut scheduler = Scheduler::new(); scheduler.prefer_rt(); // Request SCHED_FIFO (needs root or CAP_SYS_NICE) ``` ### `.cores()` — CPU pinning Pin a node to specific CPU cores for cache locality and reduced jitter: ```rust scheduler.add(controller) .rate(100.hz()) .cores(&[2, 3]) // Run only on cores 2 and 3 .build()?; ``` ### Watchdog — Frozen node detection The scheduler's watchdog detects nodes that stop responding and triggers graduated degradation (warn, reduce rate, isolate, safe state): ```rust let mut scheduler = Scheduler::new(); scheduler.watchdog(500.ms()); // Fire if any node is silent for 500ms scheduler.max_deadline_misses(3); // Enter safe mode after 3 consecutive misses ``` ## When You Do NOT Need Real-Time Not every node needs RT. Using it where you do not need it wastes CPU and adds complexity. **Prototyping.** Just get it working first. Add timing constraints later. **Simulation.** Simulated time does not care about wall-clock deadlines. **Logging and recording.** A blackbox recorder can buffer and flush at its own pace. **Visualization.** Rendering at 30fps does not need deadline enforcement. **Planning and decision-making.** A path planner that runs at 1Hz is fine as best-effort. For these, use `.compute()` (CPU-heavy work without deadlines) or just the default `BestEffort`: ```rust // No RT needed — just run when there's time scheduler.add(logger).build()?; // CPU-heavy but no deadline scheduler.add(path_planner).compute().build()?; ``` ## Quick Reference | Your node does... | Use this | Why | |---|---|---| | Motor control at 100Hz+ | `.rate(100.hz())` | Auto-derives budget and deadline | | Sensor fusion with strict timing | `.rate(200.hz()).budget(3.ms())` | Explicit budget for tight loops | | Safety-critical stop logic | `.rate(100.hz()).on_miss(Miss::SafeMode)` | Degrades safely on overrun | | ML inference (variable latency) | `.compute()` | No deadline — just use available CPU | | Event-driven message handling | `.on("topic_name")` | Runs only when data arrives | | Background logging | default (no config) | BestEffort is fine | | Visualization / UI | default or `.rate(30.hz())` | Low rate, no deadline needed | ## Next Steps - [Choosing Your Configuration](/concepts/choosing-configuration) — Practical guide to picking the right settings - [Scheduler](/concepts/core-concepts-scheduler) — Full scheduler API reference - [Nodes](/concepts/core-concepts-nodes) — Node trait and lifecycle --- ## Topics: How Nodes Talk Path: /concepts/topics-beginner Description: Understanding HORUS topics and pub/sub communication — a 5-minute introduction # Topics: How Nodes Talk > For the full reference with capacity tuning, performance optimization, and multi-process details, see [Topics — Full Reference](/concepts/core-concepts-topic). ## What is a Topic? A **topic** is a named channel that carries one type of data between nodes. Think of it like a mailbox with a specific name: - `"temperature"` carries `f32` temperature readings - `"camera.image"` carries image frames - `"motor.command"` carries velocity commands Any node can **publish** (send) to a topic. Any node can **subscribe** (receive) from it. The topic handles all the memory management automatically. ## Basic Usage ### Publishing Data ```rust use horus::prelude::*; struct Thermometer { publisher: Topic, } impl Node for Thermometer { fn tick(&mut self) { let temp = read_sensor(); // Your sensor code self.publisher.send(temp); // Send to topic } } ``` ### Receiving Data ```rust struct Display { subscriber: Topic, } impl Node for Display { fn tick(&mut self) { if let Some(temp) = self.subscriber.recv() { println!("Temperature: {:.1}", temp); } } } ``` Both nodes create a `Topic` with the same name — HORUS connects them automatically. **In Python:** ```python import horus def sensor_tick(node): node.send("temperature", 25.0) def display_tick(node): temp = node.recv("temperature") # Returns value or None if temp is not None: print(f"Temperature: {temp:.1f}") sensor = horus.Node(name="Sensor", tick=sensor_tick, pubs=["temperature"]) display = horus.Node(name="Display", tick=display_tick, subs=["temperature"]) horus.run(sensor, display) ``` ## How It Works |send| T["Shared Memory
topic: temperature"] T -->|recv| B["MonitorNode"] T -->|recv| C["LoggerNode"] end style T fill:#3b82f6,stroke:#2563eb,color:#fff `} caption="Topics use shared memory — multiple subscribers can read the same data" /> Key facts: - **Latency**: ~3ns (same thread) to ~167ns (cross-process) - **Zero-copy**: Large data (images, point clouds) is shared, not copied - **Automatic**: HORUS picks the fastest path based on where your nodes run - **One-to-many**: Multiple subscribers can receive from the same topic ## Creating Topics Topics are created in your node's `init()` or constructor: ```rust impl SensorNode { fn new() -> Result { Ok(Self { // Same constructor for publishing AND subscribing topic: Topic::new("sensor.data")?, }) } } ``` Topic names are simple strings. Convention: use dots for namespacing (`"robot.sensors.imu"`). Do not use slashes — they cause errors on macOS. ## What Happens When There's No Subscriber? `send()` always succeeds — even if nobody is listening. Data goes into shared memory and waits. When a subscriber connects later, it receives the latest value. `recv()` returns `None` if no data has been published yet. ```rust fn tick(&mut self) { // Always safe — never blocks, never panics self.publisher.send(42.0); // Returns None if no data yet match self.subscriber.recv() { Some(value) => println!("Got: {}", value), None => {} // Nothing published yet, that's fine } } ``` ## Key Takeaways - A **topic** = named channel for one data type - `send()` to publish, `recv()` to subscribe - Same topic name = automatic connection - Latency is ~87ns on average — fast enough for any control loop - `send()` never fails, `recv()` returns `None` if no data ## Next Steps - [Nodes: The Building Blocks](/concepts/nodes-beginner) — what nodes are and how they work - [Scheduler: Running Your Nodes](/concepts/scheduler-beginner) — execution order and timing - [Quick Start](/getting-started/quick-start) — build a working example with topics - [Topics — Full Reference](/concepts/core-concepts-topic) — capacity, performance, multi-process details --- ## horus.toml: Single Source of Truth Path: /concepts/horus-toml Description: Why horus.toml replaces Cargo.toml and pyproject.toml — one config file for all languages # horus.toml: Single Source of Truth Every HORUS project has one config file: `horus.toml`. It replaces the need for separate `Cargo.toml` and `pyproject.toml` files. You declare your dependencies once, and HORUS generates everything else. --- ## The Problem Traditional robotics projects need different config files for each language: | Language | Config Files | |----------|-------------| | Rust | `Cargo.toml` + `Cargo.lock` | | Python | `pyproject.toml` + `requirements.txt` | | Mixed | All of the above | A robot using Python for ML and Rust for control needs **3+ config files**. Adding a dependency means knowing which file to edit. ## The HORUS Solution One file. All languages. All dependencies. ```toml # horus.toml — the only config file you edit [package] name = "my-robot" version = "0.1.0" [dependencies] # Rust (auto-detected as crates.io) serde = { version = "1.0", source = "crates.io", features = ["derive"] } # Python (auto-detected as PyPI) numpy = { version = ">=1.24", source = "pypi" } [scripts] sim = "horus sim start --world warehouse" deploy = "horus deploy pi@robot --release" [hooks] pre_run = ["fmt", "lint"] # Auto-format and lint before every run ``` When you run `horus build`, HORUS reads `horus.toml` and generates native build files: - **Rust deps** → `.horus/Cargo.toml` - **Python deps** → `.horus/pyproject.toml` You never see or edit these generated files. --- ## The `.horus/` Directory Every HORUS project has a `.horus/` directory containing generated files and build artifacts. It's gitignored and fully managed by horus. ``` my_project/ ├── horus.toml ← You edit this ├── src/ │ ├── main.rs │ └── main.py └── .horus/ ← Generated (don't touch) ├── Cargo.toml ← From horus.toml Rust deps ├── pyproject.toml ← From horus.toml Python deps ├── target/ ← Rust build artifacts └── packages/ ← Cached registry packages ``` If `.horus/` gets corrupted, just delete it: ```bash horus clean --all # Remove everything, regenerated on next build ``` --- ## How It Works ``` horus.toml │ ┌───────────┴───────────┐ ▼ ▼ cargo_gen pyproject_gen │ │ ▼ ▼ .horus/Cargo.toml .horus/pyproject.toml ``` | Command | What happens | |---------|-------------| | `horus add serde` | Detects crates.io → adds to horus.toml → regenerates .horus/Cargo.toml | | `horus add numpy` | Detects PyPI → adds to horus.toml → regenerates .horus/pyproject.toml | | `horus build` | Reads horus.toml → generates all build files → builds all languages | | `horus test` | Runs cargo test + pytest (all from horus.toml) | | `horus remove X` | Removes from horus.toml → regenerates affected build files | --- ## Comparison | | Traditional | HORUS | |---|---|---| | **Config files** | 3+ per language | 1 (`horus.toml`) | | **Add a dep** | Edit the right file, know the syntax | `horus add NAME` | | **Build** | `cargo build` + `pip install` + ... | `horus build` | | **Test** | `cargo test` + `pytest` | `horus test` | | **Deploy** | Manual cross-compile scripts | `horus deploy pi@robot` | | **Onboarding** | Learn 3 build systems | Learn 1 CLI | --- ## Next Steps - **[Configuration Reference](/package-management/configuration)** — Full field reference for horus.toml - **[Quick Start](/getting-started/quick-start)** — Build your first project - **[CLI Reference](/development/cli-reference)** — All horus commands --- ## Scheduler: Running Your Nodes Path: /concepts/scheduler-beginner Description: How the HORUS scheduler executes your nodes — a 5-minute introduction # Scheduler: Running Your Nodes > For the full reference with real-time configuration, watchdog, deadline monitoring, and composable builders, see [Scheduler — Full Reference](/concepts/core-concepts-scheduler). ## What is the Scheduler? The **scheduler** is the engine that runs your nodes. It: 1. Calls `init()` on every node (once) 2. Calls `tick()` on every node (repeatedly, in order) 3. Calls `shutdown()` on every node when you press Ctrl+C You don't write loops or manage threads. You add nodes, set their order, and let the scheduler handle the rest. ## Basic Usage ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Add nodes with execution order scheduler.add(SensorNode::new()?) .order(0) // Runs first .build()?; scheduler.add(ControlNode::new()?) .order(1) // Runs second .build()?; scheduler.add(LoggerNode::new()?) .order(2) // Runs third .build()?; // Run until Ctrl+C scheduler.run()?; Ok(()) } ``` **In Python:** ```python import horus sensor = horus.Node(name="Sensor", tick=sensor_tick, order=0) control = horus.Node(name="Control", tick=control_tick, order=1) logger = horus.Node(name="Logger", tick=logger_tick, order=2) horus.run(sensor, control, logger) ``` ## Execution Order SensorNode"] --> B["order 1
ControlNode"] --> C["order 2
LoggerNode"] end C --> A style A fill:#22c55e,stroke:#16a34a,color:#000 style B fill:#3b82f6,stroke:#2563eb,color:#fff style C fill:#a855f7,stroke:#9333ea,color:#fff `} caption="Nodes execute in order every tick, then the cycle repeats" /> **Lower order number = runs first.** This is how you ensure data flows correctly: | Order | Node | Why This Order | |-------|------|----------------| | 0 | SensorNode | Read data first | | 1 | ControlNode | Process the data | | 2 | LoggerNode | Log the results | If two nodes have the same order, they run in the order they were added. ## Setting Tick Rate The default tick rate is **100 Hz**. You can change it: ```rust scheduler.tick_rate(100.hz()); // 100 ticks per second ``` Or set per-node rates: ```rust scheduler.add(FastSensor::new()?) .order(0) .rate(1000.hz()) // This node ticks at 1kHz .build()?; scheduler.add(SlowLogger::new()?) .order(1) .rate(10.hz()) // This node ticks at 10Hz .build()?; ``` The `.hz()` syntax comes from HORUS's `DurationExt` trait — `1000.hz()` means 1000 times per second. ## Graceful Shutdown When you press **Ctrl+C**, the scheduler: 1. Stops calling `tick()` 2. Calls `shutdown()` on every node (in reverse order) 3. Exits cleanly This is critical for robots — you want motors to stop and connections to close properly, even if the program is interrupted. ```rust impl Node for MotorController { fn shutdown(&mut self) -> Result<()> { self.motor.set_velocity(0.0); // Stop the motor! println!("Motor safely stopped"); Ok(()) } } ``` ## A Complete Example Putting it all together — a sensor that publishes data and a monitor that displays it: ```rust use horus::prelude::*; struct Sensor { publisher: Topic, value: f32, } impl Node for Sensor { fn name(&self) -> &str { "Sensor" } fn tick(&mut self) { self.value += 0.1; self.publisher.send(self.value); } } struct Monitor { subscriber: Topic, } impl Node for Monitor { fn name(&self) -> &str { "Monitor" } fn tick(&mut self) { if let Some(v) = self.subscriber.recv() { println!("Value: {:.1}", v); } } } fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(1.hz()); // 1 Hz for readability scheduler.add(Sensor { publisher: Topic::new("data")?, value: 0.0 }) .order(0).build()?; scheduler.add(Monitor { subscriber: Topic::new("data")? }) .order(1).build()?; scheduler.run() } ``` ## Key Takeaways - The **scheduler** runs your nodes — you don't write loops - `.order(n)` controls execution sequence (lower = first) - `.rate(n.hz())` sets tick frequency - **Ctrl+C** triggers graceful shutdown on all nodes - Shutdown runs in **reverse order** — dependent nodes stop first ## Next Steps - [Nodes: The Building Blocks](/concepts/nodes-beginner) — what nodes are and how they work - [Topics: How Nodes Talk](/concepts/topics-beginner) — pub/sub communication - [Quick Start](/getting-started/quick-start) — build a complete working example - [Scheduler — Full Reference](/concepts/core-concepts-scheduler) — real-time config, watchdog, deadline monitoring --- ## Choosing Your Configuration Path: /concepts/choosing-configuration Description: A beginner's guide to picking the right scheduler and node settings for your robot # 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: ```rust use horus::prelude::*; let mut scheduler = Scheduler::new(); scheduler.add(MyNode::new()).build()?; scheduler.run()?; ``` ```python 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: ```rust 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: ```rust // 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: ```rust 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? | Policy | Use When | |--------|----------| | `Miss::Warn` | Default. Non-critical nodes. | | `Miss::Skip` | Video encoding, logging. Drop a frame, keep going. | | `Miss::SafeMode` | Motor controllers. Reduce speed or hold position. | | `Miss::Stop` | Emergency systems. Stop everything. | **When to add `.failure_policy()`**: To control what happens when `tick()` panics or errors. | Policy | Use When | |--------|----------| | `Fatal` | Motor control, safety. Stop if broken. | | `Restart(n, backoff)` | Sensor drivers. Reconnect after crash. | | `Skip(n, cooldown)` | Logging, telemetry. Tolerate failures. | | `Ignore` | Debug output. Partial results are fine. | ## Level 4: Production Deployment For robots running unattended: ```rust 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: ```rust 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 | Method | Level | What It Does | |--------|-------|-------------| | `.tick_rate(freq)` | 1 | Global tick rate | | `.prefer_rt()` | 3 | Enable OS RT features | | `.require_rt()` | 4 | Panic if RT unavailable | | `.watchdog(Duration)` | 3 | Detect frozen nodes | | `.blackbox(size_mb)` | 4 | Crash recorder | | `.max_deadline_misses(n)` | 4 | Emergency stop threshold | | `.cores(&[usize])` | 4 | CPU core pinning | | `.deterministic(bool)` | 5 | Reproducible execution | | `.with_recording()` | 5 | Record for replay | | `.verbose(bool)` | 3 | Control log output | | `.telemetry(endpoint)` | 4 | Export metrics | ### Node Builder | Method | Level | What It Does | |--------|-------|-------------| | `.order(u32)` | 1 | Execution priority | | `.rate(Frequency)` | 1 | Tick rate (auto-RT) | | `.compute()` | 2 | CPU-bound thread pool | | `.async_io()` | 2 | I/O-bound async pool | | `.on("topic")` | 2 | Event-triggered | | `.on_miss(Miss)` | 3 | Deadline miss response (warned if no deadline) | | `.failure_policy(Policy)` | 3 | Crash recovery | | `.budget(Duration)` | 3 | Override tick budget (RT only — rejected on non-RT) | | `.deadline(Duration)` | 3 | Override deadline (RT only — rejected on non-RT) | | `.priority(i32)` | 4 | OS thread priority (RT only — warned if non-RT) | | `.core(usize)` | 4 | CPU core pinning (RT only — warned if non-RT) | | `.watchdog(Duration)` | 4 | Per-node watchdog | | `.build()` | 0 | Validate & finalize (always needed) | > **Tip:** `.build()` catches common mistakes — see [Validation & Conflicts](/concepts/execution-classes#validation--conflicts) for what's rejected vs warned. --- ## Core Concepts - Nodes Path: /concepts/core-concepts-nodes Description: Understanding HORUS nodes and their lifecycle # Nodes and Lifecycle > New to HORUS? Start with [Nodes: The Building Blocks](/concepts/nodes-beginner) for a 5-minute introduction. ## Key Takeaways After reading this guide, you will understand: - How nodes are self-contained units of computation that run in the scheduler - The Node trait's lifecycle methods (init, tick, shutdown) and when each is called - How NodeInfo provides logging, metrics, and timing context to your nodes - When to use different priority levels (0 for safety-critical, 100 for background logging) - Communication patterns (publisher, subscriber, pipeline, aggregator) for building node graphs Nodes are the fundamental building blocks of HORUS applications. Every component in your robotics system is a node - sensors, actuators, controllers, filters, and more. ## What is a Node? A node is a **self-contained unit of computation** that runs in the HORUS scheduler. Nodes communicate with each other through the Topic pub/sub system using shared memory IPC. ### Key Characteristics **Lifecycle Management**: Nodes have explicit initialization, execution, and shutdown phases **Priority-Based Execution**: Nodes run in priority order every tick cycle **Zero Boilerplate**: The `node!` macro generates all necessary boilerplate code **Type-Safe Communication**: Compile-time guarantees for message passing **Memory Safety**: Written in Rust with zero unsafe code in user-facing APIs ## The Node Trait Every HORUS node implements the `Node` trait. Here are the methods you'll use: ```rust pub trait Node: Send { // Required fn tick(&mut self); // Name (defaults to struct type name, e.g. `MotorController`) fn name(&self) -> &str { /* derived from type name */ } // Optional lifecycle fn init(&mut self) -> Result<()> { Ok(()) } // SAFETY: implement shutdown() for any node that controls actuators or holds hardware resources fn shutdown(&mut self) -> Result<()> { Ok(()) } fn on_error(&mut self, error: &str) { /* logs error */ } // Metadata (auto-generated by node! macro — rarely implemented manually) fn publishers(&self) -> Vec { Vec::new() } fn subscribers(&self) -> Vec { Vec::new() } // SAFETY: used by safety monitor when Miss::SafeMode triggers — override for actuator nodes fn is_safe_state(&self) -> bool { true } fn enter_safe_state(&mut self) { /* no-op */ } } ``` > **Note:** See the [API Reference](/rust/api) for complete method documentation. ### Required Methods **tick()**: Main execution loop called repeatedly by the scheduler. This is the only truly required method. ```rust fn tick(&mut self) { // Your node logic here } ``` ### Optional Methods **name()**: Returns a string identifying the node. The default implementation derives the name from the struct's type name (e.g., `SensorNode` → `"SensorNode"`). You can override it: ```rust fn name(&self) -> &str { "MyNode" } ``` When using the `node!` macro, the name is auto-generated from the struct name. **init()**: Called once during node startup, before the first `tick()`. Default: no-op. ```rust fn init(&mut self) -> Result<()> ``` **Returns:** `Ok(())` on success. If `Err(e)` is returned, the node is marked as failed and its `FailurePolicy` is applied (default: node is skipped, error logged). **When called:** Lazily on the first call to `scheduler.run()` or `scheduler.tick_once()`. NOT called at `.add().build()` time. ```rust fn init(&mut self) -> Result<()> { self.sensor = Sensor::open("/dev/i2c-1") .horus_context("opening IMU sensor")?; hlog!(info, "Sensor initialized"); Ok(()) } ``` **shutdown()**: Called once during graceful shutdown (Ctrl+C or `.stop()`). Default: no-op. ```rust // SAFETY: always implement shutdown() for nodes controlling motors, servos, or other actuators fn shutdown(&mut self) -> Result<()> { hlog!(info, "Node shutting down"); // Clean up resources, close connections, etc. Ok(()) } ``` **is_safe_state()**: Check if the node is in a safe state. Used by the safety monitor when `Miss::SafeMode` triggers. Override this to report your node's safety status: ```rust // SAFETY: override for actuator nodes — scheduler calls this during Miss::SafeMode fn is_safe_state(&self) -> bool { self.velocity == 0.0 && self.motor_disabled } ``` **enter_safe_state()**: Transition the node to a safe state. Called by the scheduler when `Miss::SafeMode` is active and the node misses a deadline: ```rust // SAFETY: stop all actuators and disable outputs when entering safe state fn enter_safe_state(&mut self) { self.velocity = 0.0; self.disable_motor(); hlog!(warn, "Entered safe state"); } ``` **on_error()**: Called when the node's `tick()` panics or returns an error. Default: logs the error message. ```rust fn on_error(&mut self, error: &str) ``` **Parameters:** - `error: &str` — Human-readable error description from the caught panic or error. Override to implement custom error recovery (e.g., reset sensor state, clear buffers): ```rust fn on_error(&mut self, error: &str) { hlog!(error, "Node error: {}", error); self.consecutive_errors += 1; if self.consecutive_errors > 5 { self.enter_safe_state(); } } ``` **publishers() / subscribers()**: Return metadata about which topics this node uses. Default: empty. Used by the monitor, graph visualization, and introspection CLI commands (`horus node info`). ```rust fn publishers(&self) -> Vec fn subscribers(&self) -> Vec ``` **Returns:** `Vec` where each entry has: - `topic_name: String` — The topic name (e.g., `"cmd_vel"`) - `type_name: String` — The message type name (e.g., `"CmdVel"`) When using the `node!` macro, these are auto-generated from your `pub {}` and `sub {}` blocks. When implementing `Node` manually, override them for accurate introspection: ```rust fn publishers(&self) -> Vec { vec![TopicMetadata { topic_name: "cmd_vel".to_string(), type_name: std::any::type_name::().to_string(), }] } ``` > **Note:** Per-node tick rates are set via the scheduler builder (`.rate(100_u64.hz())`) when adding the node, not on the Node trait itself. ## Node Lifecycle Nodes transition through well-defined states during their lifetime: ### NodeState The `NodeState` enum tracks which lifecycle phase a node is in: | Variant | Description | |---------|-------------| | `Uninitialized` | Created but not yet initialized | | `Initializing` | `init()` is running | | `Running` | Actively ticking | | `Stopping` | `shutdown()` is running | | `Stopped` | Clean shutdown complete | | `Error(msg)` | Error state with message | | `Crashed(msg)` | Unrecoverable crash | ### HealthStatus The `HealthStatus` enum represents the health of a running node as observed by the scheduler and safety monitor: | Variant | Description | |---------|-------------| | `Healthy` | Operating normally | | `Warning` | Degraded performance | | `Error` | Errors occurring but still running | | `Critical` | Fatal errors, about to crash | | `Unknown` | No heartbeat received | ### State Transitions INIT --> RUN --> STOP --> DONE RUN --> ERR ERR --> RUN RUN --> CRASH ERR --> CRASH `} caption="Node State Transitions" /> ### Lifecycle Example ```rust use horus::prelude::*; struct LifecycleDemo { counter: u32, } impl Node for LifecycleDemo { fn name(&self) -> &str { "LifecycleDemo" } fn init(&mut self) -> Result<()> { // NOTE: called ONCE when node starts — open resources, allocate buffers here hlog!(info, "Initializing resources"); self.counter = 0; Ok(()) } fn tick(&mut self) { // NOTE: called REPEATEDLY in main loop (~60 FPS default) self.counter += 1; hlog!(debug, "Tick #{}", self.counter); } // SAFETY: called ONCE during graceful shutdown — release resources, stop actuators here fn shutdown(&mut self) -> Result<()> { hlog!(info, "Shutting down after {} ticks", self.counter); Ok(()) } } ``` ## Logging and Metrics The scheduler tracks node state, metrics, and lifecycle internally. You don't interact with internal tracking directly — instead, use the `hlog!` macro for logging and the scheduler API for metrics. ### Logging with hlog! Use the `hlog!` macro for structured logging: **Info**: General information messages ```rust hlog!(info, "Robot ready"); ``` **Warn**: Warning messages that don't stop execution ```rust hlog!(warn, "Battery low"); ``` **Error**: Error messages ```rust hlog!(error, "Sensor disconnected"); ``` **Debug**: Detailed debugging information ```rust hlog!(debug, "Position: ({}, {})", x, y); ``` ### Pub/Sub Logging With the zero-overhead IPC, `send()` and `recv()` don't take any context parameter. For introspection, use CLI tools instead: ```rust fn tick(&mut self) { self.velocity_pub.send(1.5); // IMPORTANT: call recv() every tick to avoid stale data accumulation if let Some(scan) = self.lidar_sub.recv() { self.process(scan); } } ``` For monitoring without code changes, use CLI tools: `horus topic echo`, `horus topic hz`, `horus monitor`. ### Performance Metrics NodeInfo tracks detailed performance metrics: ```rust pub struct NodeMetrics { pub name: String, pub order: u32, pub total_ticks: u64, pub successful_ticks: u64, pub failed_ticks: u64, pub avg_tick_duration_ms: f64, pub max_tick_duration_ms: f64, pub min_tick_duration_ms: f64, pub last_tick_duration_ms: f64, pub messages_sent: u64, pub messages_received: u64, pub errors_count: u64, pub warnings_count: u64, pub uptime_seconds: f64, } ``` Access metrics via the scheduler: ```rust fn init(&mut self) -> Result<()> { hlog!(info, "Node initializing"); Ok(()) } fn tick(&mut self) { // Track state internally if needed self.tick_count += 1; } ``` ### Tick Timing The scheduler automatically tracks tick duration and updates metrics for each node. You don't need to call any timing methods manually. ## Node Priority Nodes execute in **priority order** each tick cycle: ### Priority Levels Priorities are represented as `u32` values where **lower numbers = higher priority**. Common priority values: ```rust // Recommended priority constants const CRITICAL: u32 = 0; // Highest priority const HIGH: u32 = 10; const NORMAL: u32 = 50; // Default const LOW: u32 = 80; const BACKGROUND: u32 = 100; // Lowest priority ``` You can use **any u32 value** for fine-grained control (e.g., 5, 15, 25, 37, 42, etc.). ### Priority Usage ```rust use horus::prelude::*; let mut scheduler = Scheduler::new(); // Safety monitor runs FIRST every tick (order 0) scheduler.add(safety_node).order(0).build()?; // Controller runs second (order 10) scheduler.add(control_node).order(10).build()?; // Sensors run third (order 50) scheduler.add(sensor_node).order(50).build()?; // Logging runs LAST (order 100) scheduler.add(logger_node).order(100).build()?; // Fine-grained priorities for complex systems scheduler.add(emergency_stop).order(0).build()?; // Highest scheduler.add(motor_control).order(15).build()?; // Between HIGH and NORMAL scheduler.add(vision_processing).order(55).build()?; // Slightly lower than normal scheduler.add(telemetry).order(90).build()?; // Between LOW and BACKGROUND ``` ### Priority Guidelines **0 (Critical)**: Safety monitors, emergency stops, fault detection **10 (High)**: Control loops, actuator commands, real-time feedback **50 (Normal)**: Sensor processing, state estimation, path planning **80 (Low)**: Non-critical computation, filtering, analysis **100 (Background)**: Logging, monitoring, diagnostics, data recording **Custom Values**: Use any `u32` value for fine-grained priority control in complex systems ## Creating Nodes ### Manual Implementation ```rust use horus::prelude::*; struct SensorNode { data_pub: Topic, } impl SensorNode { fn new() -> Result { Ok(Self { data_pub: Topic::new("sensor_data")?, }) } } impl Node for SensorNode { fn name(&self) -> &str { "SensorNode" } fn tick(&mut self) { let data = 42.0; // Read sensor self.data_pub.send(data); } // SAFETY: no actuators controlled — shutdown is optional for pure publisher nodes fn shutdown(&mut self) -> Result<()> { hlog!(info, "SensorNode stopped"); Ok(()) } } ``` ### Using the node! Macro The `node!` macro eliminates boilerplate: ```rust use horus::prelude::*; node! { SensorNode { pub { sensor_data: f32 -> "sensor_data", } tick { let data = 42.0; self.sensor_data.send(data); } } } ``` The macro generates: - Struct definition with Topic fields - Node trait implementation - Constructor function (`SensorNode::new()`) - Topic metadata methods The macro also supports `sub {}` (subscribers), `data {}` (internal state), `init {}`, `shutdown {}`, and `impl {}` blocks. See [The node! Macro Guide](/concepts/node-macro) for the full syntax including lifecycle hooks, custom names, and advanced patterns. ## Node Communication Patterns ### Publisher Pattern ```rust struct Publisher { data_pub: Topic, } impl Node for Publisher { fn tick(&mut self) { let data = self.generate_data(); self.data_pub.send(data); } // SAFETY: implement shutdown() if this publisher controls actuators fn shutdown(&mut self) -> Result<()> { Ok(()) } } ``` ### Subscriber Pattern ```rust struct Subscriber { data_sub: Topic, } impl Node for Subscriber { fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the buffer and avoid stale data if let Some(data) = self.data_sub.recv() { self.process(data); } } fn shutdown(&mut self) -> Result<()> { Ok(()) } } ``` ### Pipeline Pattern ```rust // PATTERN: Pipeline — subscribe, transform, republish struct Filter { input_sub: Topic, output_pub: Topic, } impl Node for Filter { fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the buffer and avoid stale data if let Some(input) = self.input_sub.recv() { let output = input * 2.0; self.output_pub.send(output); } } fn shutdown(&mut self) -> Result<()> { Ok(()) } } ``` ### Aggregator Pattern ```rust // PATTERN: Aggregator — combine multiple inputs into one output // NOTE: if either input has no data, BOTH recv() calls still execute but output is skipped. // This means data from the other input is consumed and lost. For independent draining, // use the Multi-Topic Synchronization pattern below instead. struct Aggregator { input_a: Topic, input_b: Topic, output_pub: Topic, } impl Node for Aggregator { fn tick(&mut self) { // IMPORTANT: both recv() calls run every tick — data is consumed even if the other is None if let (Some(a), Some(b)) = (self.input_a.recv(), self.input_b.recv()) { let result = a + b; self.output_pub.send(result); } } fn shutdown(&mut self) -> Result<()> { Ok(()) } } ``` ## Best Practices ### Keep tick() Fast The tick method should complete quickly (ideally <1ms): ```rust // IMPORTANT: keep tick() under 1ms — the scheduler monitors tick duration fn tick(&mut self) { let result = self.compute_quickly(); self.pub.send(result); } // WARNING: blocking I/O in tick() causes deadline misses — use .async_io() or .compute() instead fn tick(&mut self) { let data = std::fs::read_to_string("file.txt").unwrap(); // Blocks! // ... } ``` For slow operations, use async tasks or separate threads initialized in `init()`. ### What to Include in init() The `init()` method runs **once** when your node starts. Use it to set up everything your node needs before `tick()` begins. **Always include in init():** | Category | Examples | Why | |----------|----------|-----| | **Hardware connections** | Serial ports, I2C/SPI devices, GPIO pins | Must be opened before use | | **Network connections** | TCP/UDP sockets, WebSocket clients | Establish before tick loop | | **File handles** | Config files, log files, data files | Open once, use in tick | | **Pre-allocated buffers** | Image buffers, point cloud arrays | Avoid allocation in tick | | **Calibration/setup** | Sensor calibration, motor homing | One-time setup operations | | **Initial state** | Reset counters, clear flags | Start from known state | ```rust fn init(&mut self) -> Result<()> { hlog!(info, "Initializing MyMotorNode"); // 1. Open hardware connections self.serial_port = serialport::new("/dev/ttyUSB0", 115200) .open() .map_err(|e| Error::node("MyMotorNode", format!("Failed to open serial: {}", e)))?; // IMPORTANT: pre-allocate buffers here — allocation in tick() causes jitter self.command_buffer = vec![0u8; 256]; // 3. Initialize hardware state self.send_init_sequence()?; // SAFETY: start with actuators in a known safe state self.velocity = 0.0; self.is_armed = false; hlog!(info, "MyMotorNode initialized successfully"); Ok(()) } ``` ### What to Include in shutdown() The `shutdown()` method runs **once** when your application exits (Ctrl+C, SIGINT, SIGTERM). Use it to safely stop hardware and release resources. **Always include in shutdown():** | Category | Examples | Why | |----------|----------|-----| | **Stop actuators** | Motors, servos, pumps, valves | **CRITICAL SAFETY** - prevent runaway | | **Disable hardware** | Disable motor drivers, turn off outputs | Safe state for power-off | | **Close connections** | Serial ports, network sockets | Release system resources | | **Release GPIO** | Unexport pins, set to input mode | Allow other processes to use | | **Save state** | Log final position, save calibration | Preserve data for next run | | **Flush buffers** | Write pending data to disk | Prevent data loss | ```rust // SAFETY: shutdown() is called once on SIGINT/SIGTERM — stop all actuators before releasing resources fn shutdown(&mut self) -> Result<()> { hlog!(info, "MyMotorNode shutting down"); // CRITICAL: stop all actuators FIRST — before closing connections or releasing resources self.velocity = 0.0; self.send_stop_command(); // SAFETY: disable hardware outputs to prevent runaway on power cycle self.disable_motor_driver(); // 3. Close hardware connections if let Some(port) = self.serial_port.take() { drop(port); // Closes the port } // 4. Save any important state self.save_position_to_file()?; hlog!(info, "MyMotorNode shutdown complete"); Ok(()) } ``` ### Complete Custom Node Example Here's a complete example showing proper `init()` and `shutdown()` implementation: ```rust use horus::prelude::*; struct MyMotorController { // Hardware serial_port: Option>, // Communication cmd_sub: Topic, status_pub: Topic, // State velocity: f64, position: i32, is_enabled: bool, } impl MyMotorController { fn new() -> Result { Ok(Self { serial_port: None, cmd_sub: Topic::new("motor.cmd")?, status_pub: Topic::new("motor.status")?, velocity: 0.0, position: 0, is_enabled: false, }) } fn send_velocity(&mut self, vel: f64) { if let Some(ref mut port) = self.serial_port { let cmd = format!("V{}\n", vel); let _ = port.write(cmd.as_bytes()); } } } impl Node for MyMotorController { fn name(&self) -> &str { "MyMotorController" } fn init(&mut self) -> Result<()> { hlog!(info, "Opening serial connection to motor controller"); // Open hardware connection self.serial_port = Some( serialport::new("/dev/ttyUSB0", 115200) .timeout(std::time::Duration::from_millis(100)) .open() .map_err(|e| Error::node("MyMotorController", format!("Serial open failed: {}", e)))? ); // Initialize motor to stopped state self.send_velocity(0.0); self.is_enabled = true; hlog!(info, "Motor controller ready"); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the command buffer if let Some(cmd) = self.cmd_sub.recv() { self.velocity = cmd.velocity; self.send_velocity(self.velocity); } // Publish status let status = MotorStatus { velocity: self.velocity, position: self.position, }; self.status_pub.send(status); } // SAFETY: must stop motors before releasing serial port fn shutdown(&mut self) -> Result<()> { hlog!(info, "Stopping motor for safe shutdown"); // CRITICAL: stop motor first — before closing serial port self.velocity = 0.0; self.send_velocity(0.0); // Close serial port self.serial_port = None; self.is_enabled = false; hlog!(info, "Motor stopped safely"); Ok(()) } } ``` ### When init() and shutdown() Are NOT Optional While the default implementations are no-ops, you **should** implement them when: | Scenario | init() Required | shutdown() Required | |----------|-----------------|---------------------| | Controls motors/actuators | Setup | **YES - SAFETY CRITICAL** | | Opens serial/I2C/SPI ports | **YES** | **YES** | | Uses GPIO pins | **YES** | **YES** | | Opens network connections | **YES** | Recommended | | Allocates large buffers | **YES** | No | | Reads config files | **YES** | No | | Writes log/data files | Optional | **YES** (flush) | | Pure computation node | No | No | ### Use Result Types Return errors from init() and shutdown(): ```rust fn init(&mut self) -> Result<()> { if !self.sensor.is_available() { return Err(Error::node("MyNode", "Sensor not found")); } Ok(()) } ``` ### Use hlog! for Logging in tick() Since `tick()` no longer receives `ctx`, use the `hlog!` macro for logging: ```rust fn tick(&mut self) { hlog!(info, "Processing data"); hlog!(debug, "Detailed debug info: {:?}", self.state); } ``` ### Avoid State in Static Variables Store state in the node struct, not static variables: ```rust // IMPORTANT: store state in the node struct — the scheduler owns your node's lifetime struct MyNode { counter: u32, // Instance state } // WARNING: static mut is unsound and breaks multi-node isolation static mut COUNTER: u32 = 0; // Unsafe global state ``` ## Error Handling ### Initialization Errors ```rust fn init(&mut self) -> Result<()> { self.device = Device::open().map_err(|e| { Error::node("MyNode", format!("Failed to open device: {}", e)) })?; hlog!(info, "Device opened successfully"); Ok(()) } ``` If init() returns an error, the node transitions to **Error** state and won't run. ### Runtime Errors Handle errors in tick() without panicking: ```rust fn tick(&mut self) { // IMPORTANT: call recv() every tick — None is normal (no new data this tick) match self.data_sub.recv() { Some(data) => self.process(data), None => { // No data available - this is normal } } } ``` ### Shutdown Errors ```rust // SAFETY: never panic in shutdown — log errors and continue cleanup fn shutdown(&mut self) -> Result<()> { if let Err(e) = self.device.close() { hlog!(warn, "Failed to close device: {}", e); // Continue shutdown anyway } Ok(()) } ``` ## Advanced Topics ### Conditional Execution Run logic only under certain conditions: ```rust fn tick(&mut self) { self.tick_count += 1; // Execute every 10 ticks if self.tick_count % 10 == 0 { self.slow_operation(); } } ``` ### State-Based Logic Implement complex behavior with enum-based state patterns: ```rust // PATTERN: State machine — use enum states for complex behavior enum RobotState { Idle, Moving, Stopped, } struct RobotController { state: RobotState, cmd_sub: Topic, last_cmd: Option, } impl Node for RobotController { fn tick(&mut self) { // CRITICAL: always call recv() outside the match — calling it only in one branch // causes the buffer to fill up in other states, leading to stale commands // executing immediately on state transition self.last_cmd = self.cmd_sub.recv(); match self.state { RobotState::Idle => { if self.last_cmd.is_some() { self.state = RobotState::Moving; } } RobotState::Moving => { // Execute movement if self.is_done() { self.state = RobotState::Stopped; } } RobotState::Stopped => { self.state = RobotState::Idle; } } } // SAFETY: stop actuators on shutdown even if in Moving state fn shutdown(&mut self) -> Result<()> { self.state = RobotState::Stopped; Ok(()) } } ``` ### Multi-Topic Synchronization Wait for data from multiple topics: ```rust // PATTERN: Multi-Topic Synchronization — cache latest from each input, process when all available struct Synchronizer { topic_a: Topic, topic_b: Topic, last_a: Option, last_b: Option, } impl Node for Synchronizer { fn tick(&mut self) { // IMPORTANT: call recv() on ALL topics every tick — never skip a recv() conditionally if let Some(a) = self.topic_a.recv() { self.last_a = Some(a); } if let Some(b) = self.topic_b.recv() { self.last_b = Some(b); } // NOTE: processes with stale data from slower topic until both update if let (Some(a), Some(b)) = (self.last_a, self.last_b) { self.process(a, b); } } fn shutdown(&mut self) -> Result<()> { Ok(()) } } ``` ## Graceful Shutdown & Motor Safety When a HORUS application receives a termination signal (Ctrl+C, SIGINT, SIGTERM), the scheduler automatically calls `shutdown()` on all registered nodes. This is critical for robotics safety. ### Signal Handling The scheduler intercepts termination signals and ensures proper cleanup: (SIGINT/SIGTERM)"] FLAG["scheduler.running = false"] BREAK["Break out of tick loop"] SHUT["For each node (priority order):
node.shutdown()"] EXIT["Application exits cleanly"] CTRL --> SIG --> FLAG --> BREAK --> SHUT --> EXIT `} caption="Signal Handling Flow" /> ### Why shutdown() Matters for Motors **Without shutdown()**: If you stop your robot with Ctrl+C while motors are running, they continue at their last commanded velocity - potentially dangerous for autonomous vehicles! **With shutdown()**: Motors receive stop commands before the application exits: ```rust // SAFETY: without this, motors continue at last commanded velocity after Ctrl+C fn shutdown(&mut self) -> Result<()> { hlog!(info, "Stopping all motors for safe shutdown"); // CRITICAL: send stop command to all motors FIRST self.emergency_stop(); // SAFETY: disable motor drivers to prevent runaway self.disable_all_drivers(); hlog!(info, "Motors stopped safely"); Ok(()) } ``` ### Python Node Shutdown Behavior Python nodes also support shutdown callbacks. When the scheduler stops, your `shutdown` function runs automatically: ```python import horus velocity = [0.0, 0.0] def motor_tick(node): msg = node.recv("cmd_vel") if msg is not None: velocity[0], velocity[1] = msg def motor_shutdown(node): velocity[0], velocity[1] = 0.0, 0.0 print("Motors stopped safely") motor = horus.Node(name="MotorController", tick=motor_tick, shutdown=motor_shutdown, order=10, subs=["cmd_vel"]) horus.run(motor) ``` See the [Python Bindings](/python/api/python-bindings) documentation for details. ### Implementing shutdown() in Custom Nodes Always implement `shutdown()` for nodes that control actuators: ```rust impl Node for MyMotorController { fn name(&self) -> &str { "MyMotorController" } fn tick(&mut self) { // Normal operation - motors running self.motor_pub.send(self.velocity); } // SAFETY: called on SIGINT/SIGTERM — stop actuators before releasing hardware fn shutdown(&mut self) -> Result<()> { hlog!(info, "MyMotorController shutting down"); // CRITICAL: stop all motors FIRST — before closing any connections self.velocity = 0.0; self.motor_pub.send(0.0); // Close hardware connections if let Some(port) = self.serial_port.take() { port.close(); } hlog!(info, "All motors stopped safely"); Ok(()) } } ``` ### Testing Shutdown Behavior Test your shutdown implementation before deploying: ```bash # Start your application horus run # In another terminal, send SIGINT kill -SIGINT # Or simply press Ctrl+C in the application terminal ``` Verify in logs: ``` [12:34:56.789] [INFO] MyMotorController shutting down [12:34:56.790] [INFO] All motors stopped safely ``` ### Best Practices for Shutdown 1. **Always stop actuators first** - Motors, servos, and other actuators should receive stop commands 2. **Close hardware connections** - Serial ports, I2C, SPI, CAN bus connections 3. **Release system resources** - GPIO pins, file handles, network sockets 4. **Log shutdown progress** - Helps debug shutdown issues 5. **Don't panic in shutdown** - Handle errors gracefully, continue cleanup ```rust // SAFETY: never panic in shutdown — always attempt all cleanup steps fn shutdown(&mut self) -> Result<()> { // CRITICAL: always try to stop motors, even if other cleanup fails if let Err(e) = self.stop_motors() { hlog!(error, "Failed to stop motors: {}", e); // Continue with other cleanup anyway } // Close connections (non-critical) if let Err(e) = self.close_connection() { hlog!(warn, "Failed to close connection: {}", e); } Ok(()) } ``` ## Next Steps - Learn about [Topic and Pub/Sub](/concepts/core-concepts-topic) for inter-node communication - Understand the [Scheduler](/concepts/core-concepts-scheduler) for orchestrating nodes - Explore [Message Types](/concepts/message-types) for standard robotics data - Read the [API Reference](/rust/api) for complete Node trait documentation --- ## Execution Classes Path: /concepts/execution-classes Description: The 5 execution classes — Rt, Compute, Event, AsyncIo, BestEffort — and when to use each # Execution Classes Every node in HORUS runs in one of 5 **execution classes**. Each class has a different executor optimized for its workload. The scheduler auto-selects the class based on how you configure the node. ```rust use horus::prelude::*; let mut sched = Scheduler::new().tick_rate(100_u64.hz()); // BestEffort (default) — main tick loop sched.add(logger).order(100).build()?; // Rt (auto-detected) — dedicated RT thread sched.add(motor).order(0).rate(1000_u64.hz()).budget(300_u64.us()).build()?; // Compute — parallel thread pool sched.add(planner).order(5).compute().build()?; // Event — triggered by topic sched.add(handler).order(10).on("emergency_stop").build()?; // AsyncIo — tokio runtime sched.add(uploader).order(50).async_io().build()?; ``` --- ## BestEffort (Default) The default class. Nodes tick sequentially in the main loop, ordered by `.order()`. ```rust sched.add(display_node) .order(100) .build()?; // No .rate(), .compute(), .on(), or .async_io() → BestEffort ``` **Executor:** Main thread, sequential, round-robin. **Use for:** - Logging, telemetry, display - Simple nodes with no timing requirements - Anything that doesn't need parallelism or real-time guarantees **Characteristics:** - Runs at the scheduler's global `tick_rate()` - Sequential ordering is deterministic - Lowest overhead (no thread spawn, no synchronization) --- ## Rt (Real-Time) Nodes that need guaranteed timing. **Auto-detected** when you set `.rate()`, `.budget()`, or `.deadline()` on the node builder. ```rust // Auto-derived: budget = 80% of period, deadline = 95% of period sched.add(motor_ctrl) .order(0) .rate(1000_u64.hz()) .build()?; // Explicit budget and deadline sched.add(safety_monitor) .order(1) .rate(500_u64.hz()) .budget(800_u64.us()) .deadline(1800_u64.us()) .on_miss(Miss::Skip) .build()?; ``` **Executor:** Dedicated RT thread per node. If the OS supports it (Linux with `CAP_SYS_NICE`), uses `SCHED_FIFO` real-time scheduling. **Auto-detection rules:** - `.rate(freq)` alone → RT with auto-derived budget (80% of period) and deadline (95% of period) - `.budget(duration)` → RT - `.deadline(duration)` → RT - Any combination of the above → RT **You do NOT call `.rt()`** — there is no such method. RT is always implicit from timing constraints. **Miss policies** (what happens when a tick exceeds its deadline): | Policy | Behavior | |--------|----------| | `Miss::Warn` | Log a warning, continue normally | | `Miss::Skip` | Skip the next tick to catch up | | `Miss::SafeMode` | Reduce rate and isolate the node | | `Miss::Stop` | Emergency shutdown | **Additional RT configuration:** ```rust sched.add(critical_node) .order(0) .rate(1000_u64.hz()) .budget(300_u64.us()) .deadline(900_u64.us()) .on_miss(Miss::Skip) .priority(90) // OS-level priority (1-99, higher = more urgent) .core(0) // Pin to CPU core 0 .watchdog(500_u64.ms()) // Per-node freeze detection .build()?; ``` **Use for:** - Motor control loops (500-1000 Hz) - Safety monitoring - Any node that needs timing guarantees --- ## Compute For CPU-heavy work that benefits from parallelism. Runs on a shared thread pool. ```rust sched.add(path_planner) .order(5) .compute() .rate(10_u64.hz()) // Optional: limit compute rate .build()?; ``` **Executor:** `rayon`-style parallel thread pool. Multiple compute nodes can run simultaneously on different cores. **Use for:** - Path planning - Point cloud processing - Image processing / ML inference (CPU) - Any CPU-bound work that takes >1ms per tick **Characteristics:** - Runs in parallel with other compute nodes - Does not block the main tick loop - Optional `.rate()` limits how often the node ticks (otherwise ticks every scheduler cycle) --- ## Event Nodes that only tick when a specific topic receives new data. Zero CPU usage when idle. ```rust sched.add(estop_handler) .order(0) .on("emergency_stop") .build()?; ``` **Executor:** Wakes on topic update, runs on a dedicated thread. **Use for:** - Emergency stop handlers - Command receivers (tick only when a command arrives) - Sparse events (collision detected, goal reached) **Characteristics:** - Zero CPU when no messages arrive - Immediate wake-up on topic update (~microseconds) - The topic name in `.on("name")` must match a `Topic::new("name")` in another node --- ## AsyncIo For network, file I/O, or GPU operations that would block a real-time thread. Runs on a tokio runtime. ```rust sched.add(cloud_uploader) .order(50) .async_io() .rate(1_u64.hz()) .build()?; ``` **Executor:** `tokio::task::spawn_blocking`. The node's `tick()` runs in a blocking thread managed by tokio. **Use for:** - HTTP/REST API calls - Database writes - File I/O (logging to disk) - GPU inference (if the API blocks) - Network communication **Characteristics:** - Never blocks the main tick loop or RT threads - tokio manages the thread pool - Optional `.rate()` limits tick frequency --- ## How Classes Are Selected The scheduler selects the execution class based on **which builder methods you call**: | Builder Methods | Resulting Class | |----------------|-----------------| | (none) | BestEffort | | `.rate()` | Rt | | `.budget()` | Rt | | `.deadline()` | Rt | | `.rate()` + `.budget()` + `.deadline()` | Rt | | `.compute()` | Compute | | `.on("topic")` | Event | | `.async_io()` | AsyncIo | **Important:** `.rate()` on a `compute()` or `async_io()` node does NOT make it RT — it just limits how often the node ticks. RT is only auto-detected when `.rate()` is used without `.compute()` or `.async_io()`. --- ## Decision Guide | Your Node Does... | Use | |-------------------|-----| | Motor control at 500+ Hz | **Rt** — `.rate(500_u64.hz())` | | Safety monitoring with deadlines | **Rt** — `.rate().budget().deadline().on_miss()` | | Path planning (takes 10-50ms) | **Compute** — `.compute()` | | ML inference on CPU | **Compute** — `.compute()` | | React to emergency stop | **Event** — `.on("emergency_stop")` | | Upload telemetry to cloud | **AsyncIo** — `.async_io()` | | Write logs to disk | **AsyncIo** — `.async_io()` | | Display dashboard updates | **BestEffort** — default | | Simple sensor reading | **BestEffort** or **Rt** (if timing matters) | --- ## Complete Example: Mixed Execution Classes ```rust use horus::prelude::*; fn main() -> Result<()> { let mut sched = Scheduler::new() .tick_rate(100_u64.hz()) .prefer_rt(); // Rt — 1kHz motor control with strict timing sched.add(MotorController::new()?) .order(0) .rate(1000_u64.hz()) .budget(300_u64.us()) .on_miss(Miss::Skip) .build()?; // Rt — 100Hz sensor reading sched.add(ImuReader::new()?) .order(1) .rate(100_u64.hz()) .build()?; // Compute — path planning in parallel sched.add(PathPlanner::new()?) .order(5) .compute() .rate(10_u64.hz()) .build()?; // Event — only runs when emergency_stop topic updates sched.add(EmergencyHandler::new()?) .order(0) .on("emergency_stop") .build()?; // AsyncIo — telemetry upload every 5 seconds sched.add(TelemetryUploader::new()?) .order(50) .async_io() .rate(0.2_f64.hz()) .build()?; // BestEffort — display node in main loop sched.add(Dashboard::new()?) .order(100) .build()?; sched.run()?; Ok(()) } ``` --- ## Validation & Conflicts `.build()` validates your configuration and catches mistakes. Here's what's **rejected**, what's **warned**, and what's **valid**. ### Validity Matrix | Configuration | Result | Behavior | |---|---|---| | `.rate()` alone | **Valid** | Auto-RT with budget (80%) and deadline (95%) | | `.rate().budget()` | **Valid** | RT with explicit budget override | | `.rate().deadline()` | **Valid** | RT with explicit deadline override | | `.budget()` alone | **Valid** | Auto-RT (no rate, explicit budget) | | `.rate().compute()` | **Valid** | Compute with rate-limiting, **not** RT | | `.rate().async_io()` | **Valid** | AsyncIo with rate-limiting, **not** RT | | `.rate().on("topic")` | **Valid** | Event with rate as poll interval hint | | `.compute().budget()` | **Rejected** | Budget only meaningful for RT nodes | | `.on("topic").deadline()` | **Rejected** | Deadline only meaningful for RT nodes | | `.async_io().budget()` | **Rejected** | Budget only meaningful for RT nodes | | `.budget(Duration::ZERO)` | **Rejected** | Must be > 0 | | `.deadline(Duration::ZERO)` | **Rejected** | Must be > 0 | | `.on("")` | **Rejected** | Empty topic — node can never trigger | | `.compute().async_io()` | **Warned** | Last class wins (AsyncIo), first silently overridden | | `.compute().priority(99)` | **Warned** | Priority ignored on non-RT nodes | | `.compute().core(2)` | **Warned** | Core pinning ignored on non-RT nodes | | `.on_miss(Miss::Stop)` (no deadline) | **Warned** | No deadline to miss — policy has no effect | ### What Rejection Looks Like Rejected configurations return `Err` from `.build()`: ```rust // Budget on a non-RT node — REJECTED scheduler.add(planner) .compute() .budget(500_u64.us()) .build()?; // Error: node 'planner' has budget/deadline set but uses a non-RT execution class. // Budget/deadline are only meaningful for RT nodes. Either remove // .compute()/.async_io()/.on() or remove .budget()/.deadline(). ``` ```rust // Empty topic name — REJECTED scheduler.add(handler) .on("") .build()?; // Error: node 'handler': event topic name must not be empty — // .on("") creates a node that can never trigger. ``` ```rust // Zero deadline — REJECTED scheduler.add(motor) .rate(1000_u64.hz()) .deadline(Duration::ZERO) .build()?; // Error: deadline must be > 0 (a zero deadline is meaningless for RT guarantees) ``` ### Common Mistakes **1. Thinking `.priority()` works on Compute nodes** ```rust // ✗ Priority is silently ignored — only RT nodes get SCHED_FIFO threads scheduler.add(planner).compute().priority(99).build()?; // ✓ Make it RT if you need OS-level priority scheduler.add(planner).rate(100_u64.hz()).priority(99).build()?; ``` **2. Setting `.on_miss()` without a deadline** ```rust // ✗ No deadline means Miss::Stop can never trigger scheduler.add(controller).compute().on_miss(Miss::Stop).build()?; // ✓ Add .rate() so a deadline exists to miss scheduler.add(controller).rate(100_u64.hz()).on_miss(Miss::Stop).build()?; ``` **3. Empty topic name in `.on()`** ```rust // ✗ Empty topic — node will never trigger scheduler.add(handler).on("").build()?; // ✓ Use the actual topic name scheduler.add(handler).on("emergency_stop").build()?; ``` **4. Chaining multiple execution classes** ```rust // ✗ Only the LAST class applies — compute() is silently overridden scheduler.add(node).compute().async_io().build()?; // → AsyncIo, NOT Compute // ✓ Pick one scheduler.add(node).async_io().build()?; ``` ### What `.rate()` Actually Does The behavior of `.rate()` depends on whether you also set an execution class: | You Write | Result | |---|---| | `.rate(100.hz())` | **RT node** — auto-derives budget (80%) and deadline (95%) | | `.rate(100.hz()).compute()` | **Compute node** — rate limits ticks, no RT | | `.rate(100.hz()).async_io()` | **AsyncIo node** — rate limits ticks, no RT | | `.rate(100.hz()).on("topic")` | **Event node** — rate stored as poll interval hint | `.rate()` only auto-enables RT when no explicit execution class is set. This is intentional — you often want rate-limited compute nodes (e.g., a path planner at 10Hz) without RT overhead. --- ## See Also - [Scheduler (Full Reference)](/concepts/core-concepts-scheduler) — complete scheduler API - [Scheduler Configuration](/advanced/scheduler-configuration) — advanced tuning - [Real-Time Control Tutorial](/tutorials/realtime-control) — hands-on RT tutorial - [Choosing Configuration](/concepts/choosing-configuration) — progressive complexity guide --- ## Core Concepts - Topic Path: /concepts/core-concepts-topic Description: Understanding HORUS pub/sub communication system # Topic and Pub/Sub > New to HORUS? Start with [Topics: How Nodes Talk](/concepts/topics-beginner) for a 5-minute introduction. ## Key Takeaways After reading this guide, you will understand: - How Topic provides ultra-fast pub/sub communication (~3ns to ~167ns latency) with automatic optimization - The send() and recv() methods for publishing and subscribing to typed topics - Communication patterns (one-to-one, one-to-many, many-to-one, many-to-many) for different architectures - When to use Topic for real-time, single-machine communication vs network-based messaging The Topic is HORUS's ultra-low latency publish-subscribe (pub/sub) communication system. It enables nodes to exchange messages through shared memory IPC with **automatic optimization** — from **~3ns** (same-thread) to **~167ns** (cross-process, many-to-many) depending on topology. ## What is a Topic? A `Topic` is a **typed communication channel** that connects publishers and subscribers. The system automatically detects the optimal backend based on: - **Where** participants are (same thread, same process, or different processes) - **How many** publishers and subscribers exist You just call `send()` and `recv()` — HORUS handles the rest. ### Key Features **Automatic Optimization**: The fastest communication path is auto-detected at runtime **Zero-Copy Communication**: Simple fixed-size types are shared directly in memory without serialization **How zero-copy works:** Normally, sending a 1920x1080 camera image (6MB) between two nodes would require copying all 6MB of pixel data. With zero-copy, HORUS writes the image once to shared memory (a region of RAM that multiple processes can access directly) and sends only a small descriptor (~200 bytes) through the topic. The receiver reads the pixels from the same memory location — no copying. This is why Image topics have ~50ns latency regardless of image size. **Type Safety**: Compile-time guarantees for message types **Live Migration**: Communication paths upgrade/downgrade transparently as participants join or leave ## Automatic Optimization HORUS auto-selects the fastest communication path based on your topology. You never need to configure this — it happens automatically when participants call `send()` or `recv()`. | Scenario | Latency | |----------|---------| | Same thread | ~3ns | | Same process, 1:1 | ~18ns | | Same process, many-to-many | ~36ns | | Cross-process | ~50-167ns | > **Note**: Latencies are for small messages (16B). Larger messages scale linearly with size. If topology changes (e.g., a second subscriber joins), HORUS automatically migrates to the optimal path without dropping messages. ```rust // All of these use the same API — optimization is automatic let topic: Topic = Topic::new("velocity")?; topic.send(1.5); let msg = topic.recv(); ``` > **Tip**: Simple fixed-size structs (no `String`, `Vec`, `Box`) automatically get a faster zero-copy path. Both types work with the same API — HORUS picks the fastest path for you. ## Basic Usage ### Creating a Topic ```rust use horus::prelude::*; // Create a Topic for f32 values on topic "velocity" let topic: Topic = Topic::new("velocity")?; ``` ### Publishing Messages ```rust use horus::prelude::*; struct Publisher { velocity_pub: Topic, } impl Publisher { fn new() -> Result { Ok(Self { velocity_pub: Topic::new("velocity")?, }) } } impl Node for Publisher { fn name(&self) -> &str { "Publisher" } fn tick(&mut self) { let velocity = 1.5; // Send message — infallible, non-blocking self.velocity_pub.send(velocity); } } ``` ### Subscribing to Messages ```rust use horus::prelude::*; struct Subscriber { velocity_sub: Topic, } impl Subscriber { fn new() -> Result { Ok(Self { velocity_sub: Topic::new("velocity")?, }) } } impl Node for Subscriber { fn name(&self) -> &str { "Subscriber" } fn tick(&mut self) { if let Some(velocity) = self.velocity_sub.recv() { println!("Received velocity: {}", velocity); } } } ``` ## Transport Topics use **local shared memory** for ultra-fast communication between processes on the same machine. ```rust // Automatically uses local shared memory let topic: Topic = Topic::new("sensors")?; ``` **Performance**: ~3ns to ~167ns depending on topology **Use case**: All nodes on same machine **Pros**: Ultra-fast, deterministic, zero-copy ## API Reference > **Note**: HORUS separates introspection from the hot path. The `send()` and `recv()` methods have zero logging overhead by default. ### send() ```rust pub fn send(&self, msg: T) ``` Publishes a message to the topic. Infallible — the oldest unread message is overwritten when the buffer is full. ```rust // send() always succeeds — no error handling needed topic.send(data); ``` ### recv() ```rust pub fn recv(&self) -> Option ``` Receives the next message from the topic. Returns `None` if no message is available. Non-blocking. ```rust if let Some(data) = topic.recv() { // Process the received message } ``` ### try_send() ```rust pub fn try_send(&self, msg: T) -> Result<(), T> ``` Attempts to send a message, returning it on failure. Unlike `send()` which silently handles errors, `try_send()` gives you explicit control over failure cases. ```rust match topic.try_send(data) { Ok(()) => { /* sent successfully */ } Err(returned_data) => { /* buffer full, message returned */ } } ``` ### try_recv() ```rust pub fn try_recv(&self) -> Option ``` Attempts to receive a message. Functionally identical to `recv()` for most use cases — both are non-blocking and return `Option`. ### send_blocking() ```rust pub fn send_blocking(&self, msg: T, timeout: Duration) -> Result<(), SendBlockingError> ``` Sends a message, blocking until buffer space is available or the timeout expires. Unlike `send()` which overwrites the oldest message when full, `send_blocking()` guarantees delivery or returns an explicit error. Use this for critical command topics (emergency stop, motor setpoints) where message loss is unacceptable. ```rust use std::time::Duration; // Block up to 1ms waiting for buffer space match topic.send_blocking(emergency_stop_cmd, Duration::from_millis(1)) { Ok(()) => { /* delivered */ } Err(_) => { hlog!(error, "Failed to deliver emergency stop!"); } } ``` --- ### read_latest() ```rust pub fn read_latest(&self) -> Option where T: Copy, ``` Returns the most recent message **without advancing the consumer position**. Calling it multiple times returns the same message until a new one is published. Useful for reading infrequently-updated data like static transforms or configuration. > **`T: Copy` requirement**: `read_latest()` requires `T: Copy` to guarantee safe concurrent reads. Types with heap allocations (`String`, `Vec`, etc.) should use `recv()` instead. ```rust // Read latest transform (doesn't consume it) if let Some(transform) = tf_topic.read_latest() { self.apply_transform(transform); } ``` ### has_message() ```rust pub fn has_message(&self) -> bool ``` Checks if at least one message is available without consuming it. ### pending_count() ```rust pub fn pending_count(&self) -> u64 ``` Returns the number of messages waiting to be consumed. ### Introspection Methods Topic provides several methods for monitoring and debugging at runtime: ```rust // Topic name let name = topic.name(); // "velocity" // Check message availability if topic.has_message() { println!("{} messages pending", topic.pending_count()); } // Message loss tracking let dropped = topic.dropped_count(); if dropped > 0 { hlog!(warn, "Dropped {} messages on '{}'", dropped, topic.name()); } // Detailed metrics let metrics = topic.metrics(); println!("Sent: {}, Received: {}", metrics.messages_sent(), metrics.messages_received()); println!("Send failures: {}, Recv failures: {}", metrics.send_failures(), metrics.recv_failures()); ``` | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&str` | Topic name | | `metrics()` | `TopicMetrics` | Message counts and failure stats | | `dropped_count()` | `u64` | Messages dropped (buffer overflows) | | `has_message()` | `bool` | At least one message available | | `pending_count()` | `u64` | Messages waiting to be consumed | **TopicMetrics** provides aggregated statistics: | Method | Returns | Description | |--------|---------|-------------| | `messages_sent()` | `u64` | Total messages published | | `messages_received()` | `u64` | Total messages consumed | | `send_failures()` | `u64` | Failed send attempts | | `recv_failures()` | `u64` | Failed receive attempts | ### Runtime Debug Logging Debug logging is toggled at runtime from the TUI monitor — no code changes or recompilation needed. Select a topic in the Topics tab and press **Enter** to start logging; press **Esc** to stop. When debug logging is active, every `send()` and `recv()` records timing and message summaries to the log buffer. If the message type implements `LogSummary`, rich summaries are logged; otherwise, metadata-only entries (topic name, direction, IPC latency) are produced. ```bash horus monitor --tui # Open TUI, navigate to Topics tab, press Enter on a topic ``` Debug logging adds no overhead when disabled — introspection is fully separated from the hot path. ### with_capacity() ```rust pub fn with_capacity(name: &str, capacity: u32, slot_size: Option) -> Result ``` Creates a topic with a custom ring buffer capacity. By default, HORUS auto-sizes capacity based on the message type size. Use this when you need more buffering (e.g., bursty producers). ```rust // 256-slot ring buffer (rounded up to next power of 2) let topic: Topic = Topic::with_capacity("velocity", 256, None)?; ``` ## Type-Safe Topic Descriptors The `topics!` macro defines compile-time topic descriptors that prevent topic name typos and type mismatches across your codebase. ### Defining Topics ```rust use horus::prelude::*; // Define topics with compile-time type checking topics! { pub CMD_VEL: CmdVel = "cmd_vel", pub SENSOR_DATA: SensorReading = "sensor.data", pub MOTOR_STATUS: MotorState = "motor.status", } ``` ### Using Descriptors ```rust // Publisher — type is enforced at compile time let pub_topic: Topic = Topic::new(CMD_VEL.name())?; pub_topic.send(CmdVel::new(1.0, 0.5)); // Subscriber — same type enforced let sub_topic: Topic = Topic::new(CMD_VEL.name())?; if let Some(cmd) = sub_topic.recv() { // cmd is guaranteed to be CmdVel } ``` This prevents common errors like: - Typos in topic names (caught at compile time) - Type mismatches between publishers and subscribers - Inconsistent topic names across modules ## Communication Patterns ### One-to-One Single publisher, single subscriber (~18ns same-process, ~85ns cross-process). ```rust struct PubNode { data_pub: Topic, } impl Node for PubNode { fn name(&self) -> &str { "PubNode" } fn tick(&mut self) { self.data_pub.send(42.0); } } struct SubNode { data_sub: Topic, } impl Node for SubNode { fn name(&self) -> &str { "SubNode" } fn tick(&mut self) { if let Some(data) = self.data_sub.recv() { println!("Got: {}", data); } } } ``` ### One-to-Many (Broadcast) Single publisher, multiple subscribers (~24ns same-process, ~70ns cross-process). ```rust // One publisher struct Broadcaster { broadcast_pub: Topic, } // Multiple subscribers — all receive the same messages struct Listener1 { broadcast_sub: Topic } struct Listener2 { broadcast_sub: Topic } struct Listener3 { broadcast_sub: Topic } ``` ### Many-to-One (Aggregation) Multiple publishers, single subscriber (~26ns same-process, ~65ns cross-process). ```rust // Multiple publishers struct Sensor1 { reading_pub: Topic } struct Sensor2 { reading_pub: Topic } // Single aggregator struct Aggregator { reading_sub: Topic, } impl Node for Aggregator { fn name(&self) -> &str { "Aggregator" } fn tick(&mut self) { if let Some(reading) = self.reading_sub.recv() { self.process(reading); } } } ``` ### Many-to-Many Multiple publishers and subscribers (~36ns same-process, ~167ns cross-process). ```rust struct Agent1 { state_pub: Topic, state_sub: Topic, } struct Agent2 { state_pub: Topic, state_sub: Topic, } ``` ## Topic Naming ### Use Dots, Not Slashes > **Important**: HORUS uses **dots (`.`)** for topic hierarchy, not slashes (`/`). ```rust // CORRECT - Use dots let topic = Topic::new("sensors.lidar"); let topic = Topic::new("robot.cmd_vel"); // WRONG - Do NOT use slashes let topic = Topic::new("sensors/lidar"); // Will cause errors! let topic = Topic::new("robot/cmd_vel"); // Will cause errors! ``` **Why dots instead of slashes?** Slashes in topic names cause errors because they conflict with the underlying storage format. Always use dots. **Coming from ROS?** | Framework | Topic Separator | Example | |-----------|----------------|---------| | ROS/ROS2 | `/` | `/sensor/lidar` | | HORUS | `.` | `sensor.lidar` | ### Best Practices ```rust // Descriptive names let topic = Topic::new("cmd_vel"); // Good let topic = Topic::new("data"); // Too vague // Hierarchical naming let topic = Topic::new("sensor.lidar"); // Hierarchical let topic = Topic::new("robot1.cmd_vel"); // Namespaced let topic = Topic::new("diagnostics.cpu"); // Categorized ``` ### Reserved Topic Names Avoid using these patterns: - Topics starting with `_` (internal use) - Topics containing `/` (causes file path errors) - Topics containing `/dev/` (conflicts with paths) - Topics with special characters: `!@#$%^&*()` ## Error Handling ### Send Behavior `send()` is infallible — it uses ring buffer "keep last" semantics. When the buffer is full, the oldest message is overwritten: ```rust // send() always succeeds — no error handling needed topic.send(data); ``` For explicit error handling, use `try_send()`: ```rust match topic.try_send(data) { Ok(()) => { /* success */ } Err(msg) => { /* buffer full — msg returned to caller */ } } ``` ### Receive Behavior `recv()` returns `None` when no message is available — this is normal, not an error: ```rust match topic.recv() { Some(data) => { // Process data } None => { // No data available - this is normal } } ``` ## Message Types ### What Types Can I Use? Most types work with `Topic` — just add standard derives: ```rust use serde::{Serialize, Deserialize}; // Primitive types work out of the box let topic: Topic = Topic::new("float_topic")?; let topic: Topic = Topic::new("bool_topic")?; // Custom structs — just add derives #[derive(Clone, Serialize, Deserialize)] struct MyMessage { x: f32, y: f32, name: String, } let topic: Topic = Topic::new("my_topic")?; // Collections work too #[derive(Clone, Serialize, Deserialize)] struct SensorBatch { readings: Vec, timestamp: u64, } let topic: Topic = Topic::new("batch_topic")?; ``` ### Required Traits Your message types need these traits (all auto-derived): | Trait | Why | How to Get It | |-------|-----|---------------| | `Clone` | Messages may be copied between backends | `#[derive(Clone)]` | | `Serialize` | For serialized shared memory path | `#[derive(Serialize)]` | | `Deserialize` | For serialized shared memory path | `#[derive(Deserialize)]` | Additionally, types must be `Send + Sync + 'static` — satisfied automatically by most types. ```rust use serde::{Serialize, Deserialize}; #[derive(Clone, Serialize, Deserialize)] struct MyMessage { // your fields } ``` ## Advanced Usage ### Conditional Publishing Only publish when certain conditions are met: ```rust impl Node for ConditionalPublisher { fn tick(&mut self) { let data = self.read_sensor(); if data > self.threshold { self.alert_pub.send(data); } } } ``` ### Message Buffering Cache the last received message using `read_latest()` or manual buffering: ```rust struct BufferedSubscriber { data_sub: Topic, last_value: Option, } impl Node for BufferedSubscriber { fn tick(&mut self) { if let Some(value) = self.data_sub.recv() { self.last_value = Some(value); } if let Some(value) = self.last_value { self.process(value); } } } ``` For `Copy` types, use `read_latest()` instead — it reads the latest message without consuming it: ```rust // Requires T: Copy — reads latest without advancing consumer position if let Some(transform) = self.tf_sub.read_latest() { self.apply_transform(transform); } ``` ### Rate Limiting Publish at a specific rate: ```rust struct RateLimitedPublisher { data_pub: Topic, tick_count: u32, publish_every_n_ticks: u32, } impl Node for RateLimitedPublisher { fn tick(&mut self) { self.tick_count += 1; if self.tick_count % self.publish_every_n_ticks == 0 { self.data_pub.send(42.0); } } } ``` ### Message Filtering Filter messages before processing: ```rust impl Node for FilteringSubscriber { fn tick(&mut self) { if let Some(data) = self.data_sub.recv() { if data.is_valid() && data.quality > 0.8 { self.process(data); } } } } ``` ## Memory and Capacity ### Memory Usage Per Topic By default, `Topic::new()` auto-sizes capacity based on message type size: | Message Type | Auto Capacity | Approximate Memory | |--------------|---------------|--------------------| | `f32` (4 bytes) | 1024 slots | ~73 KB | | `CmdVel` (16 bytes) | 256 slots | ~19 KB | | `Pose2D` (32 bytes) | 128 slots | ~10 KB | | `Twist` (56 bytes) | 73 slots | ~6 KB | | `Imu` (~300 bytes) | 16 slots | ~129 KB | For most robotics applications, memory usage per topic is under 1 MB. ### Cleaning Up Shared memory files persist after processes exit (by design — allows new processes to join existing topics). Clean up between sessions: ```bash # Clean shared memory only (recommended) horus clean --shm # Preview what would be cleaned horus clean --shm --dry-run # Clean everything (shared memory + build cache) horus clean --all ``` HORUS also performs automatic stale-topic cleanup — files with no active process are removed automatically. ### Troubleshooting **"No space left on device"** — Shared memory is full: ```bash horus clean --shm # Clean up ``` **Type mismatch** — Ensure publisher and subscriber use the exact same type for a topic name: ```rust // Both sides MUST use the same type let pub_topic: Topic = Topic::new("cmd_vel")?; let sub_topic: Topic = Topic::new("cmd_vel")?; ``` ## Performance ### Latency by Topology (16B message) | Scenario | Latency | |----------|---------| | Same thread | ~3ns | | Same process, 1:1 | ~18ns | | Same process, many-to-many | ~36ns | | Cross-process | ~50-167ns | ### Latency by Message Size (cross-process) | Message Type | Size | Latency | |--------------|------|---------| | CmdVel | 16B | ~167ns | | IMU | 304B | ~940ns | | LaserScan | 1.5KB | ~2.2µs | | PointCloud | 120KB | ~360µs | Latency scales linearly with message size. ### Throughput - **Millions of messages per second** for small messages - **Gigabytes per second** for large messages - **Deterministic latency** regardless of system load ## Best Practices **Use simple fixed-size types when possible** — they get the fastest path automatically: ```rust Topic::<[f32; 3]>::new("position")?; // Fixed-size: fastest path Topic::>::new("position")?; // Dynamic: still fast, but uses serialization ``` **Keep messages small** — latency scales linearly with size. **Check recv() every tick** — don't skip ticks: ```rust fn tick(&mut self) { if let Some(msg) = self.sub.recv() { self.process(msg); } } ``` **Use topics! macro** for shared topic definitions across modules: ```rust topics! { pub CMD_VEL: CmdVel = "cmd_vel", pub ODOM: Odometry = "odom", } ``` **send() is infallible** — no error handling needed: ```rust self.pub_topic.send(data); ``` ## Next Steps - Learn about the [Scheduler](/concepts/core-concepts-scheduler) for orchestrating nodes - Explore [Message Types](/concepts/message-types) for standard robotics messages - Read the [Topic API Reference](/rust/api/topic) for complete method documentation ## See Also - [Topic API Reference](/rust/api/topic) - Complete method documentation - [Message Types](/concepts/message-types) - Standard robotics message types - [Communication Overview](/concepts/communication-overview) - How HORUS IPC works end-to-end - [Python Topic API](/python/api/python-bindings) - Using topics from Python --- ## Core Concepts - Scheduler Path: /concepts/core-concepts-scheduler Description: Understanding HORUS priority-based scheduler and execution orchestration # Scheduler > New to HORUS? Start with [Scheduler: Running Your Nodes](/concepts/scheduler-beginner) for a 5-minute introduction. ## Key Takeaways After reading this guide, you will understand: - How the Scheduler orchestrates node execution through init(), tick(), and shutdown() phases - `Scheduler::new()` as the single entry point, with builder methods for global settings - Per-node execution classes: `.compute()`, `.on(topic)`, `.async_io()` — plus auto-RT from `.budget()` / `.rate()` - The fluent `.add(node).order(0).build()` chain for adding and configuring nodes - Per-node configuration: `.order()`, `.rate()`, `.budget()`, `.deadline()`, `.on_miss()` - Composable scheduler builders: `.watchdog(Duration)`, `.blackbox(n)`, `.require_rt()`, `.max_deadline_misses(n)` - Priority-based execution where lower numbers run first (0 = highest priority) - Graceful shutdown via Ctrl+C signal handling The Scheduler is the **execution orchestrator** in HORUS. It manages the node lifecycle, coordinates priority-based execution, and handles graceful shutdown. ## What is the Scheduler? The Scheduler is responsible for: **Node Registration**: Adding nodes with the fluent builder API **Lifecycle Management**: Calling init(), tick(), and shutdown() at the right times **Priority-Based Execution**: Running nodes in priority order every tick **Signal Handling**: Graceful shutdown on Ctrl+C **Performance Monitoring**: Tracking execution metrics for all nodes **Failure Policies**: Per-node failure handling (fatal, restart, skip, ignore) ## Creating a Scheduler ### Scheduler::new() — Lightweight, No Syscalls `new()` detects runtime capabilities (~30-100us) but does **not** apply any OS-level features. Use builder methods to opt in to features. ```rust use horus::prelude::*; // Minimal — just capability detection, no syscalls let mut scheduler = Scheduler::new(); scheduler.add(my_node).order(0).build()?; scheduler.run()?; ``` ### Builder Methods — Global Settings For production deployments, combine builder methods for the right settings: ```rust use horus::prelude::*; // Production robot — watchdog, blackbox, 1 kHz control loop let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) // frozen node detection .blackbox(64) // 64 MB flight recorder .tick_rate(1000_u64.hz()); // 1 kHz control loop scheduler.add(motor_ctrl).order(0).rate(1000_u64.hz()).build()?; scheduler.run()?; ``` All RT features that cannot be applied at runtime are recorded as degradations, not errors (see [Real-Time Features](#real-time-features) below). ### Composable Builders Instead of presets, combine builder methods for your deployment needs: | Builder Method | What it enables | When to use | |--------|----------------|-------------| | `.watchdog(Duration)` | Frozen node detection — auto-creates safety monitor | Production robots | | `.blackbox(size_mb)` | BlackBox flight recorder with n MB buffer | Post-mortem debugging | | `.max_deadline_misses(n)` | Emergency stop after n deadline misses (default: 100) | Safety-critical | | `.require_rt()` | All RT features + memory locking + RT scheduling class. **Panics** without RT capabilities | Hard real-time systems | | `.prefer_rt()` | Request RT features (degrades gracefully if unavailable) | Best-effort RT | | `.verbose(bool)` | Enable/disable non-emergency logging (default: true) | Quieter production logs | | `.with_recording()` | Enable record/replay | Session recording | ## Adding Nodes Use the fluent builder API to add nodes with configuration: ```rust let mut scheduler = Scheduler::new(); // Basic: just set execution order scheduler.add(sensor_node).order(0).build()?; // With per-node tick rate — auto-derives budget (80%) and deadline (95%), auto-marks as RT scheduler.add(fast_sensor).order(0).rate(1000_u64.hz()).build()?; // Real-time node with explicit budget and deadline (auto-marks as RT) scheduler.add(motor_ctrl) .order(0) .budget(500_u64.us()) // 500μs max execution time → auto RT .deadline(1_u64.ms()) // 1ms deadline .on_miss(Miss::Skip) // Skip tick on deadline miss .build()?; // Chain multiple nodes scheduler.add(safety_node).order(0).build()?; scheduler.add(controller).order(10).build()?; scheduler.add(sensor).order(50).build()?; scheduler.add(logger).order(200).build()?; ``` ### Node Configuration **Execution classes** (mutually exclusive — pick one per node): | Method | Description | |--------|-------------| | `.compute()` | Mark as CPU-heavy compute node (may be scheduled on worker threads) | | `.on(topic)` | Event-driven — node ticks only when the given topic receives a message | | `.async_io()` | Async I/O node (non-blocking, suitable for network / file operations) | > **Note:** RT is auto-detected when you set `.budget()`, `.deadline()`, or `.rate(Frequency)`. There is no `.rt()` method — just set a budget or rate and the node is automatically marked as real-time. The default execution class is `BestEffort`. **Per-node configuration:** | Method | Description | |--------|-------------| | `.order(n)` | Set execution order (lower = runs first) | | `.rate(Frequency)` | Set tick rate (e.g. `1000_u64.hz()`) — auto-derives budget (80% period) and deadline (95% period), auto-marks RT | | `.budget(Duration)` | Set tick budget (e.g. `200_u64.us()`) — auto-marks as RT. If deadline not set, deadline = budget | | `.deadline(Duration)` | Set hard deadline (e.g. `1_u64.ms()`) — auto-marks as RT. When exceeded, `Miss` policy fires | | `.on_miss(Miss)` | What to do on deadline miss: `Warn`, `Skip`, `SafeMode`, `Stop` | | `.failure_policy(policy)` | Override failure handling policy | | `.build()` | Finalize and register the node (returns `Result`) | ## Priority-Based Execution Priorities are `u32` values where **lower numbers = higher priority**: | Range | Level | Use Case | |-------|-------|----------| | 0-9 | Critical | Safety monitors, emergency stops, watchdogs | | 10-49 | High | Control loops, actuators, time-sensitive operations | | 50-99 | Normal | Sensors, filters, state estimation | | 100-199 | Low | Logging, diagnostics, non-critical computation | | 200+ | Background | Telemetry, data recording | Nodes execute **in priority order** every tick: ```rust let mut scheduler = Scheduler::new(); // Execution order: Safety -> Controller -> Sensor -> Logger scheduler.add(safety_monitor).order(0).build()?; // Runs 1st scheduler.add(controller).order(10).build()?; // Runs 2nd scheduler.add(sensor).order(50).build()?; // Runs 3rd scheduler.add(logger).order(200).build()?; // Runs 4th ``` ## Running the Scheduler ### Continuous Mode Run until Ctrl+C: ```rust scheduler.run()?; ``` ### Duration-Limited Run for a specific duration, then shutdown: ```rust use std::time::Duration; scheduler.run_for(Duration::from_secs(30))?; ``` ### Node-Specific Execution Execute only specific nodes by name: ```rust // Run only these nodes continuously scheduler.tick(&["SensorNode", "MotorNode"])?; // Run specific nodes for a duration scheduler.tick_for(&["SensorNode"], Duration::from_secs(10))?; ``` ## Builder Methods ### Tick Rate ```rust // Set global tick rate (default: 100 Hz) let scheduler = Scheduler::new() .tick_rate(1000_u64.hz()); // 1kHz control loop ``` ### Real-Time Features RT is configured through composable builder methods. Nodes are automatically marked as RT when you set `.budget()`, `.deadline()`, or `.rate(Frequency)`: ```rust // Hard real-time — panics without RT capabilities let mut scheduler = Scheduler::new() .require_rt() .watchdog(500_u64.ms()) .tick_rate(1000_u64.hz()); // Per-node: rate auto-derives budget + deadline, auto-marks as RT // Each RT node runs on its own isolated thread — a stalled node // cannot block other RT nodes. scheduler.add(motor_ctrl) .order(0) .rate(1000_u64.hz()) // budget=800us, deadline=950us → auto RT .on_miss(Miss::SafeMode) // Enter safe mode on deadline miss .build()?; scheduler.add(sensor) .order(50) .compute() // CPU-heavy, non-RT .build()?; ``` When using `.require_rt()`, the scheduler applies (in order): 1. **RT priority** (SCHED_FIFO) — if available 2. **Memory locking** (mlockall) — if permitted 3. **CPU affinity** — only to isolated CPUs if they exist Features that fail are recorded as degradations, not errors. Use `.prefer_rt()` instead if you want graceful degradation. ### BlackBox Flight Recorder Enable BlackBox recording through the builder API: ```rust let mut scheduler = Scheduler::new() .blackbox(16); // 16 MB flight recorder buffer // All node ticks are now recorded to the flight recorder ``` ### Safety Monitor `.watchdog(Duration)` enables frozen node detection and the safety monitor. Budget enforcement is implicit when nodes have `.rate()` set: ```rust let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) // frozen node detection .blackbox(64) // 64 MB flight recorder .tick_rate(1000_u64.hz()); // RT node with auto-derived budget and deadline from rate scheduler.add(motor_ctrl) .order(0) .rate(1000_u64.hz()) // budget=800us, deadline=950us .on_miss(Miss::SafeMode) // Enter safe mode on miss .build()?; ``` ## Per-Node Rate Control Individual nodes can run at different frequencies: ```rust let mut scheduler = Scheduler::new(); scheduler.add(fast_sensor).order(0).rate(1000_u64.hz()).build()?; // 1kHz scheduler.add(slow_logger).order(200).rate(10_u64.hz()).build()?; // 10Hz // Or set rates after adding scheduler.set_node_rate("FastSensor", 100_u64.hz()); scheduler.set_node_rate("SlowLogger", 10_u64.hz()); ``` ### Ergonomic Timing with DurationExt HORUS provides extension methods on numeric types for creating `Duration` and `Frequency` values: ```rust use horus::prelude::*; // Duration helpers — works on u64 let budget = 200_u64.us(); // Duration::from_micros(200) let deadline = 1_u64.ms(); // Duration::from_millis(1) // Frequency — auto-derives budget (80% period) and deadline (95% period) let freq = 500_u64.hz(); scheduler.add(motor_ctrl) .order(0) .rate(freq) // auto-marks as RT, sets budget + deadline .on_miss(Miss::Skip) .build()?; ``` See [DurationExt and Frequency](/advanced/scheduler-configuration#durationext-and-frequency) for the full API reference. The scheduler automatically adjusts its internal tick period to be fast enough for the highest-frequency node. ## Lifecycle Management ### Initialization Phase When you call `run()`, the scheduler initializes all nodes by calling `init()`: - All nodes initialize before the main loop starts - If `init()` fails, the node enters Error state and won't tick - Other nodes continue normally ### Main Execution Loop ``` 1. Initialize all nodes (call init()) 2. Sort nodes by priority 3. Main loop: a. For each node (in priority order): - Check if node should tick (rate limiting) - Start tick timing - Call node.tick() - Record metrics, check budget/deadlines b. Sleep to maintain target tick rate 4. On shutdown signal: a. Set running = false b. Call shutdown() on all nodes ``` ### Graceful Shutdown Ctrl+C is automatically caught: ``` ^C Ctrl+C received! Shutting down HORUS scheduler... [Nodes shutting down gracefully...] Scheduler shutdown complete ``` - Main loop exits - Each node's `shutdown()` is called - RT threads are given 3 seconds to exit cleanly; stalled threads are detached - Errors during shutdown are logged but don't prevent other nodes from cleaning up - Shared memory cleaned up ## Recording and Replay ### Recording Sessions Enable recording through the builder API: ```rust let mut scheduler = Scheduler::new() .blackbox(16); // 16 MB flight recorder buffer scheduler.add(my_node).order(0).build()?; // Run normally — all node ticks are recorded scheduler.run()?; ``` ### Replaying ```rust let mut replay_scheduler = Scheduler::replay_from( "~/.horus/recordings/my_session".into() )?; replay_scheduler.run()?; ``` ## Performance Monitoring ### Node Metrics ```rust let metrics = scheduler.metrics(); for m in &metrics { println!("Node: {} (order: {})", m.name(), m.order()); println!(" Ticks: {} total, {} ok, {} failed", m.total_ticks(), m.successful_ticks(), m.failed_ticks()); println!(" Duration: avg={:.2}ms, min={:.2}ms, max={:.2}ms", m.avg_tick_duration_ms(), m.min_tick_duration_ms(), m.max_tick_duration_ms()); } ``` ### Other Monitoring Methods | Method | Description | |--------|-------------| | `metrics()` | Get `Vec` for all nodes | | `node_list()` | Get list of registered node names | | `safety_stats()` | Get budget overruns, deadline misses, watchdog expirations | | `is_running()` | Check if scheduler is running | ## Error Handling ### Node Initialization Failure If `init()` fails, the node enters Error state and won't tick. Other nodes continue: ```rust fn init(&mut self) -> Result<()> { Err(Error::node("MySensor", "Sensor not connected")) // Node won't run, others unaffected } ``` ### Runtime Errors Handle errors gracefully in `tick()` — don't panic: ```rust // GOOD: Handle errors fn tick(&mut self) { match self.operation() { Ok(_) => {} Err(e) => hlog!(error, "Error: {}", e), } } // BAD: Panic crashes the scheduler fn tick(&mut self) { self.operation().unwrap(); // Will crash! } ``` ## Common Patterns ### Layered Architecture ```rust // Layer 1: Safety (Critical) scheduler.add(collision_detector).order(0).build()?; scheduler.add(emergency_stop).order(0).build()?; // Layer 2: Control (High) scheduler.add(pid_controller).order(10).build()?; scheduler.add(motor_driver).order(10).build()?; // Layer 3: Sensing (Normal) scheduler.add(lidar_node).order(50).build()?; scheduler.add(camera_node).order(50).build()?; // Layer 4: Processing (Low) scheduler.add(path_planner).order(100).build()?; // Layer 5: Monitoring (Background) scheduler.add(logger).order(200).build()?; scheduler.add(diagnostics).order(200).build()?; ``` ### Production Deployment ```rust let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()); scheduler.add(safety_monitor).order(0).rate(1000_u64.hz()).on_miss(Miss::Stop).build()?; scheduler.add(motor_ctrl).order(5).rate(1000_u64.hz()).on_miss(Miss::SafeMode).build()?; scheduler.add(sensor).order(50).compute().build()?; scheduler.add(logger).order(200).async_io().build()?; scheduler.run()?; ``` ## Best Practices **Initialize heavy resources in init()** — not in the constructor: ```rust fn init(&mut self) -> Result<()> { self.buffer = vec![0.0; 10000]; self.connection = connect_to_hardware()?; Ok(()) } ``` **Keep tick() fast** — each tick should complete within the tick period: ```rust fn tick(&mut self) { let data = self.read_sensor(); self.pub_topic.send(data); // Fast! } ``` **Use appropriate priorities** — don't make everything order 0: ```rust scheduler.add(emergency_stop).order(0).build()?; // Critical scheduler.add(controller).order(10).build()?; // High scheduler.add(sensor).order(50).build()?; // Normal scheduler.add(logger).order(200).build()?; // Background ``` **Use composable builders for production** — enable watchdog, blackbox, and safety monitoring: ```rust // Use composable builders for production let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(16) .tick_rate(500_u64.hz()); ``` ## Miss — Deadline Miss Policy When a real-time node exceeds its deadline, the `Miss` policy determines what happens: ```rust use horus::prelude::*; scheduler.add(motor_ctrl) .order(0) .budget(500.us()) .deadline(1.ms()) .on_miss(Miss::SafeMode) // Enter safe mode on miss .build()?; ``` | Policy | Behavior | Use For | |--------|----------|---------| | `Miss::Warn` | Log a warning and continue normally | Soft real-time nodes (logging, UI) | | `Miss::Skip` | Skip the node for this tick | Firm real-time nodes (video encoding) | | `Miss::SafeMode` | Call `enter_safe_state()` on the node | Motor controllers, safety nodes | | `Miss::Stop` | Stop the entire scheduler | Hard real-time safety-critical nodes | The default is `Miss::Warn`. ## Method Reference ### Constructor & Configuration | Method | Returns | Description | |--------|---------|-------------| | `Scheduler::new()` | `Scheduler` | Create scheduler with auto-detected capabilities | | `.tick_rate(freq)` | `Self` | Set global tick rate (default: 100 Hz) | | `.watchdog(Duration)` | `Self` | Frozen node detection — auto-creates safety monitor | | `.blackbox(size_mb)` | `Self` | BlackBox flight recorder with n MB buffer | | `.max_deadline_misses(n)` | `Self` | Max deadline misses before emergency stop (default: 100) | | `.prefer_rt()` | `Self` | Try RT features, degrade gracefully if unavailable | | `.require_rt()` | `Self` | Enable RT features, panic if unavailable | | `.verbose(bool)` | `Self` | Enable/disable non-emergency logging (default: true) | | `.with_recording()` | `Self` | Enable record/replay | ### Node Registration | Method | Returns | Description | |--------|---------|-------------| | `.add(node)` | chainable | Add a node, then chain `.order()`, `.rate()`, `.build()` | ### Execution Control | Method | Returns | Description | |--------|---------|-------------| | `.run()` | `Result<()>` | Main loop — run until Ctrl+C | | `.run_for(duration)` | `Result<()>` | Run for specific duration, then shutdown | | `.tick_once()` | `Result<()>` | Execute exactly one tick cycle (no loop, no sleep) | | `.tick(&[names])` | `Result<()>` | One tick cycle for specific nodes only | | `.stop()` | `()` | Stop the scheduler | | `.is_running()` | `bool` | Check if scheduler is currently running | ### Monitoring & Statistics | Method | Returns | Description | |--------|---------|-------------| | `.metrics()` | `Vec` | Performance metrics for all nodes | | `.safety_stats()` | `Option` | Budget overruns, deadline misses, watchdog expirations | | `.node_list()` | `Vec` | List of registered node names | | `.status()` | `String` | Formatted status report (platform, RT features, safety) | ### Runtime Configuration | Method | Returns | Description | |--------|---------|-------------| | `.set_node_rate(name, freq)` | `&mut Self` | Change per-node rate at runtime | ## Next Steps - Learn about [Message Types](/concepts/message-types) for communication - Explore [Examples](/rust/examples/basic-examples) for complete applications - Read the [API Reference](/rust/api) for detailed documentation - See [Scheduler Configuration](/advanced/scheduler-configuration) for advanced options ## See Also - [Scheduler API Reference](/rust/api/scheduler) - Complete method documentation - [Execution Classes](/concepts/execution-classes) - Compute, Event, AsyncIo, and RT nodes - [Node Concepts](/concepts/core-concepts-nodes) - Deep dive into the Node pattern - [Safety Monitor](/advanced/safety-monitor) - Watchdog, budget enforcement, and degradation --- ## Message Types Path: /concepts/message-types Description: Standard HORUS message types for robotics applications # Message Types HORUS provides 70+ standard message types for robotics. Pick the right ones for your robot: | I'm building a... | Start with these messages | |---|---| | **Mobile robot** | `CmdVel`, `Odometry`, `LaserScan`, `Imu`, `BatteryState` | | **Robot arm** | `JointState`, `JointCommand`, `WrenchStamped`, `TrajectoryPoint` | | **Drone** | `Imu`, `NavSatFix`, `MotorCommand`, `BatteryState`, `Pose3D` | | **Vision system** | `Image`, `Detection`, `PointCloud`, `DepthImage`, `CameraInfo` | | **Multi-robot** | `Pose2D`, `Heartbeat`, `DiagnosticStatus`, `TransformStamped` | | **Teleoperation** | `JoystickInput`, `CmdVel`, `EmergencyStop` | | **Industrial** | `JointState`, `MotorCommand`, `ForceCommand`, `DiagnosticReport` | **Need a custom type?** Use the `message!` macro — it handles serialization and optimization automatically: ```rust use horus::prelude::*; message! { pub struct MotorFeedback { pub motor_id: u32, pub velocity: f32, pub current_amps: f32, pub temperature_c: f32, } } let topic: Topic = Topic::new("motor.feedback")?; ``` All built-in messages use fixed-size structures with zero-copy shared memory transport (~50ns latency). For zero-copy performance, make your type POD — see [POD Types](/concepts/core-concepts-podtopic) for details. --- ## Typed Messages vs Generic Messages ### Typed Messages (Recommended) Strongly-typed Rust structs — all available via `use horus::prelude::*`: ```rust use horus::prelude::*; let topic: Topic = Topic::new("robot.pose")?; topic.send(Pose2D::new(1.0, 2.0, 0.5)); ``` ```python from horus import Topic, Pose2D topic = Topic(Pose2D) topic.send(Pose2D(x=1.0, y=2.0, theta=0.5)) ``` **Benefits:** - **Ultra-fast**: ~50-167ns IPC latency (zero-copy shared memory for POD types) - **Type safety**: Compile-time checks prevent type mismatches - **IDE support**: Autocomplete, type hints, inline documentation - **Cross-language**: Rust and Python see the same typed data ### Generic Messages (Prototyping) Dynamic data for arbitrary structures using `GenericMessage`: ```python from horus import Topic # Generic topic — pass a string name for untyped data topic = Topic("custom_data") topic.send({ "value": 42, "notes": "testing new algorithm", "measurements": [1.2, 3.4, 5.6] }) ``` ```rust use horus::prelude::*; let topic: Topic = Topic::new("custom_data")?; let data = GenericMessage::from_value(&my_dynamic_data)?; topic.send(data); ``` `GenericMessage` uses MessagePack serialization with a 4KB maximum payload. It has an inline buffer for small messages (≤256 bytes) and an overflow buffer for larger ones. **Tradeoffs:** - Flexible — any data structure, evolving schemas - Slower IPC — serialization overhead vs zero-copy POD types - No compile-time type safety **Use generic messages** for quick prototypes, external JSON integrations, or truly dynamic schemas. **Default to typed messages** for production code. ### Performance Comparison | Feature | Typed Messages | Generic Messages | |---------|---------------|------------------| | **IPC Latency** | ~50-167ns (POD) | Higher (serialization) | | **Type Safety** | Compile-time | Runtime only | | **IDE Support** | Full autocomplete | None | | **Best For** | Production | Prototyping | --- ## LogSummary Trait The `LogSummary` trait provides human-readable summaries for logging. It is **not required** for basic `Topic::new()` usage, but is required if you want logging via `Topic::verbose flag (via TUI monitor)`. ```rust pub trait LogSummary { fn log_summary(&self) -> String; } ``` ### When is LogSummary Used? - `Topic::new("name")?` — no `LogSummary` required, no logging overhead - `Topic::new("name")?` — requires `T: LogSummary`, enables logging on send/recv When logging is active, `log_summary()` is called once per send/recv. Logs appear in the console and in `horus monitor`. ### Deriving LogSummary For most types, derive the trait to get `Debug` formatting automatically: ```rust use horus::prelude::*; #[derive(Debug, Clone, Serialize, Deserialize, LogSummary)] pub struct RobotState { pub position: [f64; 3], pub velocity: f64, pub battery_level: f32, } // log_summary() outputs: RobotState { position: [1.0, 2.0, 0.0], velocity: 1.5, battery_level: 0.85 } ``` ### Custom Implementation for Large Types For types where `Debug` output would be too large (images, point clouds, scans), implement `LogSummary` manually: ```rust use horus::prelude::*; impl LogSummary for MyLargeMessage { fn log_summary(&self) -> String { format!("MyMsg({} items, {:.2}MB)", self.count, self.size_mb()) } } ``` **Guidelines:** - Keep summaries concise — they appear inline in logs - Include units (meters, rad/s, %) to make values unambiguous - Log metadata about the message, not the full content ### Built-in LogSummary Implementations LogSummary is implemented for: - Primitive types: `f32`, `f64`, `i32`, `i64`, `u32`, `u64`, `usize`, `bool`, `String` - Messages: `CmdVel`, `CompressedImage`, `CameraInfo`, `RegionOfInterest`, `StereoInfo`, `NavSatFix`, `GenericMessage` - Descriptors: `ImageDescriptor`, `PointCloudDescriptor`, `DepthImageDescriptor`, `Tensor` - Any type that derives `#[derive(LogSummary)]` (uses `Debug` formatting) --- ## Geometry Messages Spatial primitives for position, orientation, and motion. All are POD types. ### CmdVel Basic 2D velocity command: ```rust use horus::prelude::*; let cmd = CmdVel::new(1.0, 0.5); // 1.0 m/s forward, 0.5 rad/s rotation let stop = CmdVel::zero(); let cmd = CmdVel::with_timestamp(1.0, 0.5, 123456789); ``` | Field | Type | Description | |-------|------|-------------| | `stamp_nanos` | `u64` | Timestamp in nanoseconds | | `linear` | `f32` | Forward velocity in m/s | | `angular` | `f32` | Rotation velocity in rad/s | ### Twist 3D velocity with linear and angular components: ```rust use horus::prelude::*; // 2D twist (common for mobile robots) let cmd = Twist::new_2d(1.0, 0.5); // 1.0 m/s forward, 0.5 rad/s rotation // 3D twist let cmd_3d = Twist::new( [1.0, 0.5, 0.0], // Linear velocity [x, y, z] m/s [0.0, 0.0, 0.5] // Angular velocity [roll, pitch, yaw] rad/s ); let stop = Twist::stop(); assert!(cmd.is_valid()); ``` | Field | Type | Description | |-------|------|-------------| | `linear` | `[f64; 3]` | Linear velocity in m/s | | `angular` | `[f64; 3]` | Angular velocity in rad/s | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### Pose2D 2D position and orientation for planar robots: ```rust use horus::prelude::*; let pose = Pose2D::new(1.0, 2.0, 0.5); // x=1m, y=2m, theta=0.5rad let origin = Pose2D::origin(); let distance = pose.distance_to(&origin); // Normalize angle to [-π, π] let mut pose = Pose2D::new(1.0, 2.0, 3.5); pose.normalize_angle(); ``` | Field | Type | Description | |-------|------|-------------| | `x` | `f64` | X position in meters | | `y` | `f64` | Y position in meters | | `theta` | `f64` | Orientation in radians | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### TransformStamped 3D transformation with translation and rotation: ```rust use horus::prelude::*; let identity = TransformStamped::identity(); let tf = TransformStamped::new( [1.0, 2.0, 3.0], // Translation [x, y, z] [0.0, 0.0, 0.0, 1.0] // Rotation quaternion [x, y, z, w] ); // From 2D pose let pose2d = Pose2D::new(1.0, 2.0, 0.5); let tf = TransformStamped::from_pose_2d(&pose2d); // Normalize quaternion let mut tf = tf; tf.normalize_rotation(); ``` | Field | Type | Description | |-------|------|-------------| | `translation` | `[f64; 3]` | Position in meters | | `rotation` | `[f64; 4]` | Quaternion [x, y, z, w] | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### Point3, Vector3, Quaternion 3D points, vectors, and rotations: ```rust use horus::prelude::*; // Point let point = Point3::new(1.0, 2.0, 3.0); let distance = point.distance_to(&Point3::origin()); // Vector with operations let vec = Vector3::new(1.0, 0.0, 0.0); let magnitude = vec.magnitude(); let dot = vec.dot(&Vector3::new(0.0, 1.0, 0.0)); let cross = vec.cross(&Vector3::new(0.0, 1.0, 0.0)); // Quaternion let q = Quaternion::identity(); let q = Quaternion::from_euler(0.0, 0.0, std::f64::consts::PI / 2.0); ``` --- ## Sensor Messages Standard sensor data formats. All are POD types. ### LaserScan 2D lidar scan data (up to 360 points): ```rust use horus::prelude::*; let mut scan = LaserScan::new(); scan.ranges[0] = 5.2; scan.angle_min = -std::f32::consts::PI; scan.angle_max = std::f32::consts::PI; scan.range_min = 0.1; scan.range_max = 30.0; scan.angle_increment = std::f32::consts::PI / 180.0; let angle = scan.angle_at(45); if scan.is_range_valid(0) { println!("Range: {}m", scan.ranges[0]); } let valid = scan.valid_count(); if let Some(min) = scan.min_range() { println!("Closest: {}m", min); } ``` | Field | Type | Description | |-------|------|-------------| | `ranges` | `[f32; 360]` | Range readings in meters (0 = invalid) | | `angle_min` / `angle_max` | `f32` | Scan angle range in radians | | `range_min` / `range_max` | `f32` | Valid range limits in meters | | `angle_increment` | `f32` | Angular resolution in radians | | `time_increment` | `f32` | Time between measurements | | `scan_time` | `f32` | Time to complete scan in seconds | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### Imu Inertial Measurement Unit data: ```rust use horus::prelude::*; let mut imu = Imu::new(); imu.set_orientation_from_euler(0.1, 0.2, 1.5); // roll, pitch, yaw imu.angular_velocity = [0.1, 0.2, 0.3]; // rad/s imu.linear_acceleration = [0.0, 0.0, 9.81]; // m/s² if imu.has_orientation() { let quat = imu.orientation; } assert!(imu.is_valid()); ``` | Field | Type | Description | |-------|------|-------------| | `orientation` | `[f64; 4]` | Quaternion [x, y, z, w] | | `orientation_covariance` | `[f64; 9]` | 3x3 covariance matrix | | `angular_velocity` | `[f64; 3]` | Gyroscope data in rad/s | | `angular_velocity_covariance` | `[f64; 9]` | 3x3 covariance matrix | | `linear_acceleration` | `[f64; 3]` | Accelerometer data in m/s² | | `linear_acceleration_covariance` | `[f64; 9]` | 3x3 covariance matrix | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### Odometry Combined pose and velocity from wheel encoders or visual odometry: ```rust use horus::prelude::*; let mut odom = Odometry::new(); odom.set_frames("odom", "base_link"); let pose = Pose2D::new(1.0, 2.0, 0.5); let twist = Twist::new_2d(0.5, 0.2); odom.update(pose, twist); ``` | Field | Type | Description | |-------|------|-------------| | `pose` | `Pose2D` | Current position and orientation | | `twist` | `Twist` | Current velocity | | `pose_covariance` | `[f64; 36]` | 6x6 covariance matrix | | `twist_covariance` | `[f64; 36]` | 6x6 covariance matrix | | `frame_id` | `[u8; 32]` | Reference frame (e.g., "odom") | | `child_frame_id` | `[u8; 32]` | Child frame (e.g., "base_link") | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### Range Single-point distance sensor (ultrasonic, infrared): ```rust use horus::prelude::*; let range = Range::new(Range::ULTRASONIC, 1.5); let mut range = Range::new(Range::INFRARED, 0.8); range.min_range = 0.02; range.max_range = 4.0; range.field_of_view = 0.1; ``` | Field | Type | Description | |-------|------|-------------| | `sensor_type` | `u8` | `Range::ULTRASONIC` (0) or `Range::INFRARED` (1) | | `field_of_view` | `f32` | Sensor FOV in radians | | `min_range` / `max_range` | `f32` | Valid range limits in meters | | `range` | `f32` | Distance reading in meters | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### BatteryState Battery status and charge information: ```rust use horus::prelude::*; let mut battery = BatteryState::new(12.6, 75.0); // 12.6V, 75% charge battery.current = -2.5; battery.temperature = 28.5; battery.power_supply_status = BatteryState::STATUS_DISCHARGING; if battery.is_low(20.0) { println!("Battery low!"); } if battery.is_critical() { // Below 10% println!("Battery critical!"); } if let Some(time_left) = battery.time_remaining() { println!("Time remaining: {}s", time_left); } ``` | Field | Type | Description | |-------|------|-------------| | `voltage` | `f32` | Battery voltage in volts | | `current` | `f32` | Current in amperes (negative = discharging) | | `charge` | `f32` | Remaining charge in Ah | | `capacity` | `f32` | Total capacity in Ah | | `percentage` | `f32` | Charge percentage (0-100) | | `power_supply_status` | `u8` | `STATUS_UNKNOWN` (0), `STATUS_CHARGING` (1), `STATUS_DISCHARGING` (2), `STATUS_FULL` (3) | | `temperature` | `f32` | Temperature in °C | | `cell_voltages` | `[f32; 16]` | Individual cell voltages | | `cell_count` | `u8` | Number of cells | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### NavSatFix GPS position data: | Field | Type | Description | |-------|------|-------------| | `latitude` / `longitude` / `altitude` | `f64` | WGS84 coordinates | | `position_covariance` | `[f64; 9]` | 3x3 covariance matrix | | `status` | `u8` | Fix status | | `satellites_visible` | `u16` | Number of satellites | | `hdop` / `vdop` | `f32` | Dilution of precision | | `speed` / `heading` | `f32` | Speed (m/s) and heading (rad) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | --- ## Control Messages Actuator commands and control parameters. All are POD types. ### MotorCommand Direct motor control: | Field | Type | Description | |-------|------|-------------| | `motor_id` | `u32` | Motor identifier | | `mode` | `f32` | Control mode | | `target` | `f32` | Target value | | `max_velocity` / `max_acceleration` | `f32` | Limits | | `feed_forward` | `f32` | Feed-forward term | | `enable` | `u8` | Enable flag | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### DifferentialDriveCommand Differential drive control (left/right wheels): | Field | Type | Description | |-------|------|-------------| | `left_velocity` / `right_velocity` | `f32` | Wheel velocities in m/s | | `max_acceleration` | `f32` | Acceleration limit | | `enable` | `u8` | Enable flag | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### ServoCommand Servo position/velocity control: | Field | Type | Description | |-------|------|-------------| | `servo_id` | `u32` | Servo identifier | | `position` / `speed` | `f32` | Target position and speed | | `enable` | `u8` | Enable flag | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### JointCommand Multi-joint position/velocity/effort (up to 16 joints): | Field | Type | Description | |-------|------|-------------| | `joint_names` | `[[u8; 32]; 16]` | Joint names | | `joint_count` | `u8` | Number of active joints | | `positions` / `velocities` / `efforts` | `[f64; 16]` | Joint commands | | `modes` | `[u8; 16]` | Control mode per joint | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### PidConfig PID controller parameters: | Field | Type | Description | |-------|------|-------------| | `controller_id` | `u32` | Controller identifier | | `kp` / `ki` / `kd` | `f64` | PID gains | | `integral_limit` / `output_limit` | `f64` | Limits | | `anti_windup` | `u8` | Anti-windup flag | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### TrajectoryPoint Single point in a trajectory: | Field | Type | Description | |-------|------|-------------| | `position` / `velocity` / `acceleration` | `[f64; 3]` | 3D motion | | `orientation` | `[f64; 4]` | Quaternion [x, y, z, w] | | `angular_velocity` | `[f64; 3]` | Angular velocity | | `time_from_start` | `f64` | Time offset in seconds | --- ## Vision Messages Image and camera data types. ### Image RAII image type with zero-copy shared memory backing. Pixel data lives in shared memory — only a lightweight descriptor is transmitted through topics. ```rust use horus::prelude::*; // Note: args are (height, width, encoding) let mut img = Image::new(480, 640, ImageEncoding::Rgb8)?; img.set_pixel(100, 200, &[255, 0, 0]); // Set pixel at (x=100, y=200) let pixels: &[u8] = img.data(); // Zero-copy access ``` **Accessor methods:** | Method | Returns | Description | |--------|---------|-------------| | `width()` | `u32` | Image width in pixels | | `height()` | `u32` | Image height in pixels | | `encoding()` | `ImageEncoding` | Pixel format | | `channels()` | `u32` | Number of channels | | `step()` | `u32` | Row stride in bytes | | `data()` / `data_mut()` | `&[u8]` / `&mut [u8]` | Zero-copy pixel data | | `pixel(x, y)` | `Option<&[u8]>` | Get a single pixel | | `set_pixel(x, y, val)` | `&mut Self` | Set a single pixel | | `copy_from(buf)` | `&mut Self` | Copy pixel data from buffer | | `fill(val)` | `&mut Self` | Fill entire image | | `roi(x, y, w, h)` | `Option>` | Extract region of interest | | `frame_id()` | `&str` | Camera frame | | `timestamp_ns()` | `u64` | Nanoseconds since epoch | **Supported encodings:** Mono8, Mono16, Rgb8, Bgr8, Rgba8, Bgra8, Yuv422, Mono32F, Rgb32F, BayerRggb8, Depth16 ### CompressedImage JPEG/PNG compressed images (variable-size, not POD): | Field | Type | Description | |-------|------|-------------| | `format` | `[u8; 8]` | Compression format string | | `data` | `Vec` | Compressed image data | | `width` / `height` | `u32` | Image dimensions | | `frame_id` | `[u8; 32]` | Camera frame | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### CameraInfo Camera calibration parameters (POD type): | Field | Type | Description | |-------|------|-------------| | `width` / `height` | `u32` | Image dimensions | | `distortion_model` | `[u8; 16]` | Distortion model name | | `distortion_coefficients` | `[f64; 8]` | Distortion coefficients | | `camera_matrix` | `[f64; 9]` | 3x3 intrinsic matrix | | `rectification_matrix` | `[f64; 9]` | 3x3 rectification matrix | | `projection_matrix` | `[f64; 12]` | 3x4 projection matrix | | `frame_id` | `[u8; 32]` | Camera frame | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### StereoInfo Stereo camera parameters (POD type): | Field | Type | Description | |-------|------|-------------| | `left_camera` / `right_camera` | `CameraInfo` | Per-camera calibration | | `baseline` | `f64` | Baseline distance in meters | | `depth_scale` | `f64` | Depth scaling factor | Methods: `depth_from_disparity()`, `disparity_from_depth()` --- ## Large Data (Zero-Copy) For most use cases, use the high-level domain types (`Image`, `PointCloud`, `DepthImage`) — they use zero-copy shared memory transport automatically and provide domain-specific convenience methods like `pixel()`, `point_at()`, and `get_depth()`. ```rust use horus::prelude::*; // Create an image (backed by shared memory automatically) let mut img = Image::new(1080, 1920, ImageEncoding::Rgb8)?; // ... fill pixels via img.data_mut() or img.set_pixel() ... let topic: Topic = Topic::new("camera.rgb")?; topic.send(&img); // Receiver gets zero-copy access if let Some(img) = topic.recv() { println!("{}x{} {:?}", img.width(), img.height(), img.encoding()); } ``` Only a lightweight descriptor is transmitted through topics while the actual data stays in shared memory. For low-level tensor transport (ML inference, custom pipelines), `Topic` provides direct access to the same zero-copy shared memory path with raw shape/dtype control. --- ## Detection Messages Object detection results. All are POD types. ### BoundingBox2D / BoundingBox3D 2D and 3D bounding boxes: | Type | Fields | |------|--------| | `BoundingBox2D` | `x`, `y`, `width`, `height` (all `f32`) | | `BoundingBox3D` | `cx`, `cy`, `cz` (center), `length`, `width`, `height`, `roll`, `pitch`, `yaw` (all `f32`) | ### Detection / Detection3D 2D and 3D object detections: | Field | Type | Description | |-------|------|-------------| | `bbox` | `BoundingBox2D` or `BoundingBox3D` | Bounding box | | `confidence` | `f32` | Detection confidence | | `class_id` | `u32` | Class identifier | | `class_name` | `[u8; 32]` | Class name string | | `instance_id` | `u32` | Instance identifier | `Detection3D` also includes `velocity: [f32; 3]`. --- ## Perception Messages 3D perception data types. ### PointCloud RAII point cloud type with zero-copy shared memory backing. Point data lives in shared memory — only a lightweight descriptor is transmitted through topics. ```rust use horus::prelude::*; // Create XYZ cloud: (num_points, fields_per_point, dtype) let cloud = PointCloud::from_xyz(\&points)? // 1000 points; // 1000 XYZ points let cloud = PointCloud::from_xyz(\&points) // 1000 points, 6 fields?; // 1000 XYZRGB points ``` **Accessor methods:** | Method | Returns | Description | |--------|---------|-------------| | `point_count()` | `u64` | Number of points | | `fields_per_point()` | `u32` | Floats per point (3=XYZ, 4=XYZI, 6=XYZRGB) | | `dtype()` | `TensorDtype` | Data type of components | | `is_xyz()` | `bool` | Whether this is a plain XYZ cloud | | `has_intensity()` | `bool` | Whether cloud has intensity | | `has_color()` | `bool` | Whether cloud has color | | `data()` / `data_mut()` | `&[u8]` / `&mut [u8]` | Zero-copy point data | | `point_at(idx)` | `Option<&[u8]>` | Get the i-th point as bytes | | `extract_xyz()` | `Option>` | Extract all XYZ coordinates | | `copy_from(buf)` | `&mut Self` | Copy point data from buffer | | `frame_id()` | `&str` | Reference frame | | `timestamp_ns()` | `u64` | Nanoseconds since epoch | ### Fixed-Size Point Types (POD) For zero-copy point cloud processing: | Type | Fields | Description | |------|--------|-------------| | `PointXYZ` | `x`, `y`, `z` (`f32`) | 3D point | | `PointXYZRGB` | `x`, `y`, `z` (`f32`), `r`, `g`, `b`, `a` (`u8`) | Colored point | | `PointXYZI` | `x`, `y`, `z`, `intensity` (`f32`) | Point with intensity | ### DepthImage RAII depth image type with zero-copy shared memory backing. Supports both F32 (meters) and U16 (millimeters) formats. ```rust use horus::prelude::*; let mut depth = DepthImage::meters(480, 640)?; // F32 meters depth.set_depth(100, 200, 1.5); // Set depth at (x=100, y=200) ``` **Accessor methods:** | Method | Returns | Description | |--------|---------|-------------| | `width()` / `height()` | `u32` | Image dimensions | | `dtype()` | `TensorDtype` | Data type (F32 or U16) | | `is_meters()` | `bool` | Whether F32 depth in meters | | `is_millimeters()` | `bool` | Whether U16 depth in mm | | `depth_scale()` | `f32` | Depth scale factor | | `data()` / `data_mut()` | `&[u8]` / `&mut [u8]` | Zero-copy depth data | | `get_depth(x, y)` | `Option` | Get depth at pixel (meters) | | `set_depth(x, y, val)` | `&mut Self` | Set depth at pixel | | `get_depth_u16(x, y)` | `Option` | Get raw U16 depth | | `depth_statistics()` | `(f32, f32, f32)` | (min, max, mean) in meters | | `frame_id()` | `&str` | Reference frame | | `timestamp_ns()` | `u64` | Nanoseconds since epoch | --- ## Navigation Messages Path planning and navigation types. ### Goal / GoalResult Navigation goal and result (both POD): ```rust use horus::prelude::*; let goal = Goal::new(Pose2D::new(5.0, 3.0, 0.0), 0.1, 0.05); // pose, position_tol, angle_tol let goal = goal.with_timeout(30.0).with_priority(1); if goal.is_reached(¤t_pose) { println!("Goal reached!"); } ``` **Goal fields:** `target_pose` (Pose2D), `tolerance_position`, `tolerance_angle`, `timeout_seconds` (f64), `priority` (u8), `goal_id` (u32), `timestamp_ns` (u64) **GoalResult fields:** `goal_id` (u32), `status` (u8), `distance_to_goal` (f64), `eta_seconds` (f64), `progress` (f32), `error_message` ([u8; 64]), `timestamp_ns` (u64) **GoalStatus values:** Pending, Active, Succeeded, Aborted, Cancelled, Preempted, TimedOut ### Waypoint / Path Waypoints and paths (both POD): ```rust use horus::prelude::*; let wp = Waypoint::new(Pose2D::new(1.0, 2.0, 0.0)); let wp = wp.with_velocity(Twist::new_2d(0.5, 0.0)).with_stop(); let mut path = Path::new(); path.add_waypoint(wp); ``` **Path** holds up to 256 waypoints with fields for `total_length`, `duration_seconds`, `frame_id`, `algorithm`, and `timestamp_ns`. ### PathPlan Compact planned path (POD, fixed-size): | Field | Type | Description | |-------|------|-------------| | `waypoint_data` | `[f32; 768]` | 256 waypoints × 3 floats (x, y, theta) | | `goal_pose` | `[f32; 3]` | Target pose | | `waypoint_count` | `u16` | Number of waypoints | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### OccupancyGrid 2D occupancy map (variable-size, not POD — uses serialization): ```rust let mut grid = OccupancyGrid::new(200, 200, 0.05, Pose2D::origin()); // 200x200, 5cm resolution if let Some((gx, gy)) = grid.world_to_grid(1.0, 2.0) { grid.set_occupancy(gx, gy, 100); // Mark occupied (grid coords) } // is_free/is_occupied take world coordinates directly if grid.is_free(1.0, 2.0) { /* navigable */ } if grid.is_occupied(1.0, 2.0) { /* blocked */ } ``` Values: -1 = unknown, 0 = free, 100 = occupied. ### CostMap Cost map for path planning (variable-size, not POD): ```rust let costmap = CostMap::from_occupancy_grid(grid, 0.55); // 55cm inflation radius let cost = costmap.cost(1.0, 2.0); // Get cost at world coordinates ``` ### VelocityObstacle / VelocityObstacles For velocity obstacle-based collision avoidance (both POD). `VelocityObstacles` holds up to 32 velocity obstacles. --- ## Diagnostics Messages Health monitoring and safety. All are POD types. ### Heartbeat Node liveness signal: | Field | Type | Description | |-------|------|-------------| | `node_name` | `[u8; 32]` | Node name | | `node_id` | `u32` | Node identifier | | `sequence` | `u64` | Sequence number | | `alive` | `u8` | Alive flag | | `uptime` | `f64` | Uptime in seconds | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### NodeHeartbeat Detailed per-node health status: | Field | Type | Description | |-------|------|-------------| | `state` / `health` | `u8` | Node state and health | | `tick_count` | `u32` | Total ticks executed | | `target_rate` / `actual_rate` | `f32` | Expected vs actual tick rate | | `error_count` | `u32` | Error counter | | `last_tick_timestamp` / `heartbeat_timestamp` | `u64` | Timestamps | ### Status General status report: | Field | Type | Description | |-------|------|-------------| | `level` | `u8` | Severity level | | `code` | `u32` | Status code | | `message` | `[u8; 128]` | Status message | | `component` | `[u8; 32]` | Component name | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### EmergencyStop Emergency stop signal: | Field | Type | Description | |-------|------|-------------| | `engaged` | `u8` | E-stop engaged flag | | `reason` | `[u8; 64]` | Reason string | | `source` | `[u8; 32]` | Source of e-stop | | `auto_reset` | `u8` | Auto-reset flag | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### SafetyStatus Safety system state: | Field | Type | Description | |-------|------|-------------| | `enabled`, `estop_engaged`, `watchdog_ok`, `limits_ok`, `comms_ok` | `u8` | Status flags | | `mode` | `u8` | Safety mode | | `fault_code` | `u32` | Fault code | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### ResourceUsage CPU/memory monitoring: | Field | Type | Description | |-------|------|-------------| | `cpu_percent` / `memory_percent` / `disk_percent` | `f32` | Usage percentages | | `memory_bytes` / `disk_bytes` | `u64` | Usage in bytes | | `network_tx_bytes` / `network_rx_bytes` | `u64` | Network traffic | | `temperature` | `f32` | Temperature in °C | | `thread_count` | `u32` | Thread count | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### DiagnosticValue / DiagnosticReport Key-value diagnostics: `DiagnosticValue` holds a single key-value pair. `DiagnosticReport` groups up to 16 `DiagnosticValue` entries with a component name and severity level. --- ## Force/Haptics Messages Force sensing and haptic feedback. All are POD types. | Message | Description | |---------|-------------| | `WrenchStamped` | Force/torque measurement with point of application | | `ImpedanceParameters` | Impedance control parameters (stiffness, damping, inertia) | | `ForceCommand` | Force control command with target force/torque | | `ContactInfo` | Contact detection state, force, normal, and point | | `HapticFeedback` | Haptic output command (vibration, force feedback) | --- ## Input Messages Human input devices. All are POD types. | Message | Description | |---------|-------------| | `JoystickInput` | Gamepad/joystick state (buttons, axes, hats) | | `KeyboardInput` | Keyboard key events with modifier flags | --- ## Segmentation, Landmark, and Tracking Messages Computer vision pipeline types. All are POD types. | Message | Description | |---------|-------------| | `SegmentationMask` | Semantic/instance/panoptic segmentation mask descriptor | | `Landmark` / `Landmark3D` | 2D/3D keypoints with visibility | | `LandmarkArray` | Set of landmarks (supports COCO, MediaPipe Pose/Hand/Face presets) | | `TrackedObject` | Tracked object with bbox, velocity, age, and state | | `TrackingHeader` | Tracking frame header with active track count | --- ## Custom Messages ### Basic Custom Message ```rust use serde::{Serialize, Deserialize}; use horus::prelude::*; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RobotStatus { pub battery_level: f32, pub temperature: f32, pub error_code: u32, pub timestamp_ns: u64, } let topic: Topic = Topic::new("robot_status")?; topic.send(RobotStatus { battery_level: 75.0, temperature: 42.0, error_code: 0, timestamp_ns: timestamp_now(), }); ``` ### POD Custom Message (Zero-Copy) For maximum performance, make your type POD-compatible: ```rust use horus::prelude::*; message! { MotorFeedback { timestamp_ns: u64, motor_id: u32, velocity: f32, current_amps: f32, temperature_c: f32, } } // Ready to use with Topic — zero-copy automatically let topic: Topic = Topic::new("motor.feedback")?; ``` See [POD Types](/concepts/core-concepts-podtopic) for full requirements. ### Adding LogSummary To enable logging with `Topic::verbose flag (via TUI monitor)`: ```rust use horus::prelude::*; // Option 1: Derive (uses Debug formatting) #[derive(Debug, Clone, Serialize, Deserialize, LogSummary)] pub struct SmallMessage { /* ... */ } // Option 2: Manual (for large types) impl LogSummary for LargeMessage { fn log_summary(&self) -> String { format!("LargeMsg({} items)", self.count) } } ``` --- ## Working with Messages in Nodes ### Publishing ```rust use horus::prelude::*; struct LidarNode { scan_pub: Topic, } impl Node for LidarNode { fn name(&self) -> &str { "LidarNode" } fn tick(&mut self) { let mut scan = LaserScan::new(); scan.ranges[0] = 5.2; self.scan_pub.send(scan); } } ``` ### Subscribing ```rust struct ObstacleDetector { scan_sub: Topic, } impl Node for ObstacleDetector { fn name(&self) -> &str { "ObstacleDetector" } fn tick(&mut self) { if let Some(scan) = self.scan_sub.recv() { if let Some(min_range) = scan.min_range() { if min_range < 0.5 { // Obstacle too close! } } } } } ``` --- ## GenericMessage Dynamic message type for cross-language communication (Rust <-> Python). Uses MessagePack serialization internally. Maximum payload: 4KB. ```rust use horus::prelude::*; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct SensorReading { temperature: f64, humidity: f64 } // Create from any serializable type let reading = SensorReading { temperature: 22.5, humidity: 60.0 }; let msg = GenericMessage::from_value(&reading)?; // Send through a topic let topic: Topic = Topic::new("sensor.generic")?; topic.send(msg); // Receive and deserialize if let Some(msg) = topic.recv() { let reading: SensorReading = msg.to_value()?; println!("Temperature: {}", reading.temperature); } ``` ### Key Methods | Method | Description | |--------|-------------| | `GenericMessage::new(bytes)` | Create from raw bytes | | `GenericMessage::from_value(v)` | Serialize any `Serialize` type | | `GenericMessage::with_metadata(bytes, meta)` | Create with metadata string | | `.data()` | Get raw byte payload | | `.metadata()` | Get metadata string (if set) | | `.to_value::()` | Deserialize to any `Deserialize` type | ### Performance - Small messages (≤256 bytes): ~4.0 us (inline fast path) - Large messages (>256 bytes): ~4.4 us (overflow buffer) - Maximum payload: 4096 bytes - Uses zero-copy IPC for transport ### When to Use - **Cross-language communication** — Python and Rust nodes sharing untyped data - **Prototyping** — Quick iteration before defining typed messages - **ML pipelines** — Flexible model outputs with varying schemas - **Metadata tagging** — Attach routing or context info via the metadata field For production code with known schemas, prefer typed messages for compile-time safety and ~50x lower serialization overhead. --- ## Clock & Time Messages | Message | Description | API Reference | |---------|-------------|---------------| | `Clock` | Simulation/replay time broadcast (clock_ns, sim_speed, paused) | [Clock API](/rust/api/clock-messages) | | `TimeReference` | External time sync from GPS/NTP/PTP (time_ref_ns, source, offset) | [Clock API](/rust/api/clock-messages) | ## Audio Messages | Message | Description | API Reference | |---------|-------------|---------------| | `AudioFrame` | Microphone audio data (up to 4800 samples, configurable sample rate and channels) | [AudioFrame](/stdlib/messages/audio-frame) | | `AudioEncoding` | Encoding format enum: `F32` or `I16` | [AudioFrame](/stdlib/messages/audio-frame) | --- ## See Also - **[POD Types](/concepts/core-concepts-podtopic)** — Zero-serialization for maximum performance - **[Tensor Messages](/rust/api/tensor-messages)** — Tensor, Device, TensorDtype, and tensor domain types - **[Large Data Types](/rust/api/tensor-pool)** — Tensor memory management and auto-managed pools - **[Topic](/concepts/core-concepts-topic)** — The unified communication API - **[Basic Examples](/rust/examples/basic-examples)** — Working examples with messages - **[Architecture](/concepts/architecture)** — How messages fit into HORUS --- ## node! Macro Guide Path: /concepts/node-macro Description: Write less code with the node! macro # The node! Macro **The problem**: Writing HORUS nodes manually requires lots of boilerplate code. **The solution**: The node! macro generates all the boilerplate for you! ## Why Use It? **Without the macro**: ```rust pub struct SensorNode { temperature: Topic, counter: u32, } impl SensorNode { pub fn new() -> Self { Self { temperature: Topic::new("temperature") .expect("Failed to create publisher 'temperature'"), counter: 0, } } } impl Node for SensorNode { fn name(&self) -> &str { "sensor_node" } fn tick(&mut self) { let temp = 20.0 + (self.counter as f32 * 0.1); self.temperature.send(temp); self.counter += 1; } } impl Default for SensorNode { fn default() -> Self { Self::new() } } ``` **With the macro** (13 lines): ```rust node! { SensorNode { pub { temperature: f32 -> "temperature" } data { counter: u32 = 0 } tick { let temp = 20.0 + (self.counter as f32 * 0.1); self.temperature.send(temp); self.counter += 1; } } } ``` The macro generates the struct, `new()` constructor, `Node` trait impl, and `Default` impl automatically. ## Basic Syntax ```rust node! { NodeName { name: "custom_name", // Explicit node name (optional) rate 100.0 // Tick rate in Hz (optional) pub { ... } // Publishers (optional) sub { ... } // Subscribers (optional) data { ... } // Internal state (optional) tick { ... } // Main loop (required) init { ... } // Startup (optional) shutdown { ... } // Cleanup (optional) impl { ... } // Custom methods (optional) } } ``` **Only the node name and `tick` are required!** Everything else is optional. ## Sections Explained ### `name:` - Explicit Node Name (Optional) Override the auto-generated node name with a custom identifier: ```rust node! { FlightControllerNode { name: "flight_controller", // Custom name instead of "flight_controller_node" pub { status: String -> "fc.status" } tick { ... } } } ``` **By default**, the node name is auto-generated from the struct name using snake_case: - `SensorNode` → `"sensor_node"` - `IMUProcessor` → `"i_m_u_processor"` - `MyRobotController` → `"my_robot_controller"` **With explicit naming**, you control the exact name used everywhere: - Scheduler registration and execution logs - Monitor TUI display - Log messages (`[flight_controller] ...`) - Diagnostics and metrics **Use cases for explicit naming:** - Multiple instances of the same node type: `name: "imu_front"`, `name: "imu_rear"` - Cleaner names for acronyms: `IMUSensor` → `name: "imu_sensor"` (instead of `"i_m_u_sensor"`) - Match existing naming conventions in your robot system - Shorter names for logging readability ```rust // Example: Multiple IMU sensors with explicit names node! { IMUSensor { name: "imu_front", pub { data: Imu -> "sensors.imu_front" } tick { ... } } } node! { IMUSensor { // Same struct definition... name: "imu_rear", // ...but different runtime identity pub { data: Imu -> "sensors.imu_rear" } tick { ... } } } ``` ### `rate` - Tick Rate (Optional) Specify how often this node should tick: ```rust rate 100.0 // Run at 100 Hz (100 times per second) ``` This sets the node's preferred tick rate. Common values: - `1000.0` - 1kHz for motor control loops - `100.0` - 100Hz for sensor processing - `30.0` - 30Hz for vision processing - `1.0` - 1Hz for slow monitoring If not specified, the node runs at the scheduler's global tick rate (default ~60Hz). The rate can also be overridden at runtime using `scheduler.set_node_rate("node_name", 200.0)`. ### `pub` - Send Messages Define what this node sends: ```rust pub { // Syntax: name: Type -> "topic" velocity: f32 -> "robot.velocity", status: String -> "robot.status" } ``` This creates: - A `Topic` field called `velocity` - A `Topic` field called `status` - Both connected to their respective topics ### `sub` - Receive Messages Define what this node receives: ```rust sub { // Syntax: name: Type -> "topic" commands: String -> "user.commands", sensors: f32 -> "sensors.temperature" } ``` This creates: - A `Topic` field called `commands` - A `Topic` field called `sensors` - Both listening to their respective topics ### `data` - Internal State Store data inside your node: ```rust data { counter: u32 = 0, buffer: Vec = Vec::new(), last_time: Instant = Instant::now() } ``` Access these as `self.counter`, `self.buffer`, etc. ### `tick` - Main Loop This runs repeatedly at the node's tick rate: ```rust tick { // Read inputs if let Some(cmd) = self.commands.recv() { // Process let result = process(cmd); // Send outputs self.status.send(result); } // Update state self.counter += 1; } ``` **Keep this fast!** It runs every frame. ### `init` - Startup (Optional) Runs once when your node starts: ```rust init { hlog!(info, "Starting up"); self.buffer.reserve(1000); // Pre-allocate Ok(()) } ``` The init block must return `Ok(())` on success (it generates `fn init(&mut self) -> Result<()>`). Use this for: - Opening files/connections - Pre-allocating memory - One-time setup ### `shutdown` - Cleanup (Optional) Runs once when your node stops: ```rust shutdown { hlog!(info, "Processed {} messages", self.counter); // Save state, close files, etc. Ok(()) } ``` ### `impl` - Custom Methods (Optional) Add helper functions: ```rust impl { fn calculate(&self, x: f32) -> f32 { x * 2.0 + self.counter as f32 } fn reset(&mut self) { self.counter = 0; } } ``` ## Complete Examples ### High-Rate Motor Controller ```rust node! { MotorController { rate 1000.0 // 1kHz control loop sub { target_velocity: f32 -> "motor.target" } pub { pwm_output: f32 -> "motor.pwm" } data { kp: f32 = 0.5, current_velocity: f32 = 0.0 } tick { if let Some(target) = self.target_velocity.recv() { // Simple P controller let error = target - self.current_velocity; let output = (self.kp * error).clamp(-1.0, 1.0); self.pwm_output.send(output); } } } } ``` ### Simple Publisher ```rust node! { HeartbeatNode { pub { alive: bool -> "system.heartbeat" } tick { self.alive.send(true); } } } ``` ### Simple Subscriber ```rust node! { LoggerNode { sub { messages: String -> "logs" } tick { if let Some(msg) = self.messages.recv() { hlog!(info, "[LOG] {}", msg); } } } } ``` ### Pipeline (Sub + Pub) ```rust node! { DoubleNode { sub { input: f32 -> "numbers" } pub { output: f32 -> "doubled" } tick { if let Some(num) = self.input.recv() { self.output.send(num * 2.0); } } } } ``` ### With State ```rust node! { AverageNode { sub { input: f32 -> "values" } pub { output: f32 -> "average" } data { buffer: Vec = Vec::new(), max_size: usize = 10 } tick { if let Some(value) = self.input.recv() { self.buffer.push(value); // Keep only last 10 values if self.buffer.len() > self.max_size { self.buffer.remove(0); } // Calculate average let avg: f32 = self.buffer.iter().sum::() / self.buffer.len() as f32; self.output.send(avg); } } } } ``` ### With Lifecycle ```rust node! { FileLoggerNode { sub { data: String -> "logs" } data { file: Option = None } init { use std::fs::OpenOptions; self.file = OpenOptions::new() .create(true) .append(true) .open("log.txt") .ok(); hlog!(info, "File opened"); Ok(()) } tick { if let Some(msg) = self.data.recv() { if let Some(file) = &mut self.file { use std::io::Write; writeln!(file, "{}", msg).ok(); } } } shutdown { hlog!(info, "Closing file"); self.file = None; // Closes the file Ok(()) } } } ``` ## Tips and Tricks ### Use Descriptive Names ```rust // Good pub { motor_speed: f32 -> "motors.speed" } // Bad pub { x: f32 -> "data" } ``` ### Keep tick Fast ```rust // Good - quick operation tick { if let Some(x) = self.input.recv() { let y = x * 2.0; self.output.send(y); } } // Bad - slow operation tick { std::thread::sleep(Duration::from_secs(1)); // Blocks everything! } ``` ### Pre-allocate in init() ```rust init { self.buffer.reserve(1000); // Do this once Ok(()) } tick { // Don't allocate in tick - do it in init! } ``` ## Common Questions ### Do I need to import anything? Yes, import the prelude: ```rust use horus::prelude::*; node! { MyNode { ... } } ``` ### Can I have multiple publishers? Yes! ```rust pub { speed: f32 -> "speed", direction: f32 -> "direction", status: String -> "status" } ``` ### Can I skip sections I don't need? Yes! Only `NodeName` and `tick` are required: ```rust node! { MinimalNode { tick { hlog!(info, "Hello!"); } } } ``` ### How do I use the node? Create it and add it to the scheduler: ```rust let mut scheduler = Scheduler::new(); scheduler.add(MyNode::new()).order(0).build()?; scheduler.run()?; ``` The macro generates `new() -> Self` (not `Result`). Topic creation failures panic with a descriptive error message. ## Troubleshooting ### "Cannot find type in scope" Import your message types: ```rust use horus::prelude::*; node! { MyNode { pub { cmd: CmdVel -> "cmd_vel" } ... } } ``` ### "Expected `,`, found `{`" Check your syntax: ```rust // Wrong pub { cmd: f32 "topic" } // Right pub { cmd: f32 -> "topic" } ``` ### Node name must be CamelCase ```rust // Wrong node! { my_node { ... } } // Right node! { MyNode { ... } } ``` ### How do I give my node a custom name? Use the `name:` section: ```rust node! { MyNode { name: "robot1_controller", // Custom runtime name tick { ... } } } // Now node.name() returns "robot1_controller" instead of "my_node" ``` This is useful for: - Running multiple instances of the same node type - Avoiding ugly auto-generated names (e.g., `IMU` → `"i_m_u"`) - Matching external naming conventions ## What the Macro Generates For reference, the `node!` macro expands to: 1. **`pub struct NodeName`** — with `Topic` fields for publishers/subscribers and your data fields 2. **`impl NodeName { pub fn new() -> Self }`** — constructor that creates all topics and initializes data with defaults 3. **`impl Node for NodeName`** — with `name()`, `tick()`, optional `init()`, `shutdown()`, `publishers()`, `subscribers()`, and `rate()` methods 4. **`impl Default for NodeName`** — calls `Self::new()` 5. **`impl NodeName { ... }`** — any methods from the `impl` section The struct name is converted to snake_case for the node name (e.g., `SensorNode` becomes `"sensor_node"`), unless overridden with `name:`. ## See Also - **[Core Concepts: Nodes](/concepts/core-concepts-nodes)** — Full node model and lifecycle - **[Basic Examples](/rust/examples/basic-examples)** — Real applications using the macro - **[Message Types](/concepts/message-types)** — Available message types for pub/sub - **[Topic](/concepts/core-concepts-topic)** — Communication API details --- ## Actions Path: /concepts/actions Description: Long-running tasks with feedback, cancellation, and preemption # Actions > **Beta**: The Actions API is functional in Rust but still maturing. Python bindings are not yet available. The API may change in future releases. Your robot needs to navigate across a warehouse, pick up a package, and bring it back. That takes 30 seconds. How does the operator know it's working? How do they cancel if something goes wrong? How does a new high-priority goal interrupt the current one? **Topics** can't do this — they're fire-and-forget. **Services** can't either — they block until done, with no progress updates. **Actions** solve this: ``` Operator Robot | | |--- "Go to shelf B3" --------> | | | starts navigating... |<-- "12m remaining, 20%" ----- | |<-- "8m remaining, 45%" ------ | |<-- "3m remaining, 78%" ------ | | | |--- "Cancel! New priority" --> | stops safely |<-- "Canceled at (4.2, 1.1)" - | ``` **Use actions when:** - The task takes more than one tick (navigation, arm motion, calibration) - You need progress updates (distance remaining, percent complete) - You need to cancel or preempt in-flight tasks - You need to know if the task succeeded or failed ## Defining an Action Use the `action!` macro to define Goal, Feedback, and Result types: ```rust use horus::prelude::*; action! { /// Navigate to a target position Navigate { goal { target_x: f64, target_y: f64, max_speed: f64 = 1.0, // Default value } feedback { distance_remaining: f64, percent_complete: f32, } result { success: bool, final_x: f64, final_y: f64, } } } ``` This generates: - `NavigateGoal` struct with the goal fields - `NavigateFeedback` struct with the feedback fields - `NavigateResult` struct with the result fields - `Navigate` marker type implementing the `Action` trait ### Standard Action Templates For common robotics patterns, use the `standard_action!` shortcut: ```rust standard_action!(navigate MyNavAction); // Goal: target pose, Feedback: distance, Result: final pose standard_action!(manipulate MyPickPlace); // Goal: object + target, Feedback: phase, Result: success standard_action!(wait MyWaitAction); // Goal: duration, Feedback: elapsed, Result: completed standard_action!(dock MyDockAction); // Goal: dock ID, Feedback: alignment, Result: docked ``` For a simple action with single fields per section, you can still use the `action!` macro: ```rust action! { Spin { goal { angular_velocity: f64 } feedback { current_angle: f64 } result { total_rotations: u32 } } } ``` --- ## Action Server The action server receives goals, executes them, and sends back feedback and results. ### Building a Server ```rust let server = ActionServerNode::::builder() // Validate incoming goals .on_goal(|goal| { if goal.max_speed <= 0.0 { GoalResponse::Reject("Speed must be positive".into()) } else { GoalResponse::Accept } }) // Handle cancellation requests .on_cancel(|goal_id| { hlog!(info, "Cancel requested for {:?}", goal_id); CancelResponse::Accept }) // Execute the action .on_execute(|handle| { let goal = handle.goal(); let mut distance = ((goal.target_x).powi(2) + (goal.target_y).powi(2)).sqrt(); let total = distance; while distance > 0.1 { // Check for cancellation if handle.is_cancel_requested() { return handle.canceled(NavigateResult { success: false, final_x: goal.target_x - distance, final_y: goal.target_y - distance, }); } // Simulate movement distance -= goal.max_speed * 0.1; // Publish feedback handle.publish_feedback(NavigateFeedback { distance_remaining: distance.max(0.0), percent_complete: ((total - distance) / total * 100.0) as f32, }); std::thread::sleep(std::time::Duration::from_millis(100)); } handle.succeed(NavigateResult { success: true, final_x: goal.target_x, final_y: goal.target_y, }) }) .build(); ``` ### Server Configuration ```rust let server = ActionServerNode::::builder() .on_goal(|_| GoalResponse::Accept) .on_execute(|handle| { /* ... */ handle.succeed(result) }) .max_concurrent_goals(Some(1)) // Only one goal at a time .feedback_rate(20.0) // 20 Hz feedback rate .goal_timeout(Duration::from_secs(30)) // Timeout after 30s .preemption_policy(PreemptionPolicy::PreemptOld) // New goals preempt active .build(); ``` ### Preemption Policies | Policy | Behavior | |--------|----------| | `PreemptOld` | New goals cancel the active goal (default) | | `RejectNew` | Reject new goals while one is active | | `Priority` | Higher-priority goals preempt lower-priority ones | | `Queue { max_size }` | Queue goals in FIFO order | ### ServerGoalHandle The `handle` passed to `on_execute` provides: ```rust handle.goal_id() // Unique goal identifier handle.goal() // The goal request (&A::Goal) handle.priority() // Goal priority level handle.status() // Current GoalStatus handle.elapsed() // Time since goal started handle.is_cancel_requested() // Client requested cancellation? handle.is_preempt_requested() // Higher-priority goal arrived? handle.should_abort() // Timeout or other abort condition? handle.publish_feedback(fb) // Send feedback to client // Terminal methods (consume the handle): handle.succeed(result) // -> GoalOutcome::Succeeded handle.abort(result) // -> GoalOutcome::Aborted handle.canceled(result) // -> GoalOutcome::Canceled handle.preempted(result) // -> GoalOutcome::Preempted ``` ### Server Metrics ```rust let metrics = server.metrics(); println!("Goals received: {}", metrics.goals_received); println!("Active: {}, Queued: {}", metrics.active_goals, metrics.queued_goals); println!("Succeeded: {}, Aborted: {}", metrics.goals_succeeded, metrics.goals_aborted); ``` --- ## Action Client ### Async Client (Node-Based) Use `ActionClientNode` when running inside a scheduler: ```rust let client = ActionClientNode::::builder() .on_feedback(|goal_id, feedback| { println!("Progress: {:.0}%", feedback.percent_complete); }) .on_result(|goal_id, status, result| { println!("Goal {:?} finished: {:?}", goal_id, status); }) .build(); // Send a goal let handle = client.send_goal(NavigateGoal { target_x: 5.0, target_y: 3.0, max_speed: 1.0, })?; // Or with priority let handle = client.send_goal_with_priority(goal, GoalPriority::HIGH)?; ``` ### ClientGoalHandle ```rust handle.goal_id() // Unique goal ID handle.status() // Current GoalStatus handle.is_active() // Pending or Active? handle.is_done() // In terminal state? handle.is_success() // Succeeded? handle.elapsed() // Time since sent handle.last_feedback() // Most recent feedback (Option) handle.result() // Final result if done (Option) handle.cancel() // Request cancellation // Blocking wait let result = handle.await_result(Duration::from_secs(10)); // Wait with feedback callback let result = handle.await_result_with_feedback( Duration::from_secs(10), |feedback| println!("Distance: {:.1}m", feedback.distance_remaining), )?; ``` ### Sync Client (Standalone) Use `SyncActionClient` for simple scripts without a scheduler: ```rust let client = SyncActionClient::::new()?; // Blocking call let result = client.send_goal_and_wait( NavigateGoal { target_x: 5.0, target_y: 3.0, max_speed: 1.0 }, Duration::from_secs(30), )?; // With feedback let result = client.send_goal_and_wait_with_feedback( goal, Duration::from_secs(30), |feedback| println!("{:.0}% complete", feedback.percent_complete), )?; ``` --- ## Goal Lifecycle ``` Client Server | | |--- GoalRequest -------------> | | | on_goal() → Accept/Reject |<----------- StatusUpdate --- | (Pending → Active) | | |<----------- Feedback ------- | on_execute() running |<----------- Feedback ------- | publish_feedback() |<----------- Feedback ------- | | | |--- CancelRequest ----------> | (optional) | | on_cancel() → Accept/Reject | | |<----------- Result --------- | succeed() / abort() / canceled() | | ``` ### GoalStatus | Status | Description | |--------|-------------| | `Pending` | Received but not yet executing | | `Active` | Currently executing | | `Succeeded` | Completed successfully | | `Aborted` | Failed during execution | | `Canceled` | Canceled by client request | | `Preempted` | Canceled by higher-priority goal | | `Rejected` | Rejected by `on_goal` validation | ### GoalPriority ```rust GoalPriority::HIGHEST // 0 - Critical tasks GoalPriority::HIGH // 64 GoalPriority::NORMAL // 128 (default) GoalPriority::LOW // 192 GoalPriority::LOWEST // 255 - Background tasks ``` --- ## Running Actions in a Scheduler ```rust fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Action server scheduler.add( ActionServerNode::::builder() .on_goal(|_| GoalResponse::Accept) .on_execute(|handle| { // ... navigation logic ... handle.succeed(result) }) .build() ).order(0).build()?; // Action client scheduler.add( ActionClientNode::::builder() .on_result(|_, status, result| { println!("Navigation {:?}: arrived={}", status, result.success); }) .build() ).order(1).build()?; scheduler.run() } ``` --- ## Error Handling ```rust match client.send_goal(goal) { Ok(handle) => { /* goal accepted */ } Err(ActionError::GoalRejected(reason)) => { /* validation failed */ } Err(ActionError::ServerUnavailable) => { /* no server running */ } Err(ActionError::GoalTimeout) => { /* server didn't respond */ } Err(e) => { /* other error */ } } ``` | Error | Cause | |-------|-------| | `GoalRejected(reason)` | `on_goal` returned `Reject` | | `GoalCanceled` | Goal was canceled | | `GoalPreempted` | Goal was preempted by higher priority | | `GoalTimeout` | Execution exceeded timeout | | `ServerUnavailable` | No action server found | | `CommunicationError(msg)` | IPC failure | | `ExecutionError(msg)` | Error during execution | | `InvalidGoal(msg)` | Malformed goal data | | `GoalNotFound(id)` | Unknown goal ID | --- ## CLI Commands ```bash # List active actions horus action list # Get action details horus action info navigate ``` --- ## Prebuilt Action Patterns The `standard_action!` macro provides ready-to-use action definitions for common robotics tasks: ### Navigate ```rust use horus::prelude::*; standard_action!(navigate); // Creates: NavigateGoal, NavigateFeedback, NavigateResult, Navigate let goal = NavigateGoal { target_x: 5.0, target_y: 3.0, target_theta: Some(1.57), max_speed: 1.0, }; ``` **Goal**: `target_x`, `target_y`, `target_theta` (optional), `max_speed` (default 1.0) **Feedback**: `distance_remaining`, `current_x`, `current_y`, `progress_percent` **Result**: `success`, `final_x`, `final_y`, `final_theta`, `distance_traveled`, `time_elapsed` ### Manipulate ```rust standard_action!(manipulate); // Creates: ManipulateGoal, ManipulateFeedback, ManipulateResult, Manipulate ``` **Goal**: `object_id`, `target_x/y/z`, `force_limit` (default 10.0) **Feedback**: `phase`, `grip_force`, `ee_x/y/z`, `progress_percent` **Result**: `success`, `error_message`, `object_x/y/z` ### Wait ```rust standard_action!(wait); // Creates: WaitGoal, WaitFeedback, WaitResult, Wait ``` **Goal**: `duration_secs` **Feedback**: `time_remaining`, `progress_percent` **Result**: `completed`, `actual_duration` ### Dock ```rust standard_action!(dock); // Creates: DockGoal, DockFeedback, DockResult, Dock ``` **Goal**: `dock_id`, `approach_speed` (default 0.1) **Feedback**: `phase`, `distance_to_dock`, `alignment_error`, `dock_detected` **Result**: `success`, `docked_id`, `contact_force` --- ## Next Steps - **[Actions API Reference](/rust/api/actions)** — Full method tables for ActionServerBuilder, ClientGoalHandle, GoalStatus, etc. - **[Communication Overview](/concepts/communication-overview)** — When to use topics vs actions vs services - **[Services](/concepts/services)** — Synchronous request/response pattern - **[Scheduler](/concepts/core-concepts-scheduler)** — Running action nodes --- ## Services Path: /concepts/services Description: Synchronous request/response RPC between nodes # Services > **Beta**: The Services API is functional in Rust but still maturing. Python bindings are not yet available. The API may change in future releases. Your SLAM node needs a map region from the map server. It can't continue until it gets the data. Your arm planner needs to know joint limits before computing a trajectory. Your calibration routine needs to trigger sensor calibration and wait for the result. These are **request/response** problems. Topics can't solve them — they're fire-and-forget with no reply. Actions are overkill — these operations finish in milliseconds. **Use services when:** - You need a response before continuing (parameter queries, map data, joint limits) - The operation completes quickly (within milliseconds) - You need guaranteed delivery with error reporting **Use topics instead** for continuous data streams. **Use actions instead** for long-running tasks with feedback. ## Defining a Service Use the `service!` macro to define Request and Response types: ```rust use horus::prelude::*; service! { GetMapRegion { request { x_min: f64, y_min: f64, x_max: f64, y_max: f64, resolution: f64, } response { width: u32, height: u32, data: Vec, timestamp: u64, } } } ``` This generates `GetMapRegionRequest` and `GetMapRegionResponse` structs, ready to use with clients and servers. --- ## Service Server The server listens for requests and returns responses. It runs in a background thread. ```rust let server = ServiceServerBuilder::::new() .on_request(|req| { if req.resolution <= 0.0 { return Err("Resolution must be positive".into()); } if req.x_max <= req.x_min || req.y_max <= req.y_min { return Err("Invalid region bounds".into()); } let map = generate_map(req.x_min, req.y_min, req.x_max, req.y_max, req.resolution); Ok(GetMapRegionResponse { width: map.width, height: map.height, data: map.data, timestamp: horus::timestamp_now(), }) }) .poll_interval(Duration::from_millis(1)) // How often to check for requests (default: 5ms) .build()?; // Server runs in a background thread — it's active until dropped ``` ### Stopping a Server The server stops when dropped, or explicitly: ```rust server.stop(); ``` --- ## Service Client ### Blocking Client The simplest way to call a service: ```rust let mut client = ServiceClient::::new()?; let response = client.call( GetMapRegionRequest { x_min: 0.0, y_min: 0.0, x_max: 10.0, y_max: 10.0, resolution: 0.05, }, Duration::from_secs(1), )?; println!("Map: {}x{} pixels", response.width, response.height); ``` #### Resilient Calls (Auto-Retry) Use `call_resilient` for production code that needs automatic retries on transient failures: ```rust // Auto-retry with default settings (3 retries, exponential backoff from 10ms) let response = client.call_resilient(request, Duration::from_secs(5))?; // Custom retry configuration use horus::prelude::RetryConfig; let response = client.call_resilient_with( request, Duration::from_secs(5), RetryConfig::new(5, Duration::from_millis(20)), // 5 retries, 20ms initial backoff )?; ``` `call_resilient` retries on `Timeout` and `Transport` errors. `ServiceFailed` and `NoServer` errors are not retried since they indicate permanent failures. #### Optional Response Use `call_optional` when the server may not be running: ```rust match client.call_optional(request, Duration::from_millis(100))? { Some(response) => println!("Map: {}x{}", response.width, response.height), None => println!("No server available"), } ``` ### Async Client For non-blocking calls, use `AsyncServiceClient`: ```rust let mut client = AsyncServiceClient::::new()?; // Start the call (non-blocking) let mut pending = client.call_async( GetMapRegionRequest { x_min: 0.0, y_min: 0.0, x_max: 5.0, y_max: 5.0, resolution: 0.1 }, Duration::from_secs(1), ); // Do other work... // Check if response is ready (non-blocking) match pending.check()? { Some(response) => println!("Map: {}x{}", response.width, response.height), None => println!("Still waiting..."), } // Check if the call has timed out if pending.is_expired() { println!("Service call timed out"); } // Or block until done let response = pending.wait()?; ``` ### Client Configuration ```rust // Custom poll interval for faster response detection let mut client = ServiceClient::::with_poll_interval( Duration::from_micros(500), // Default: 1ms )?; ``` --- ## Architecture Services use two internal topics for bidirectional communication: ``` Client Server | | |-- {name}.request ----------> | ServiceRequest | | handler processes request |<---------- {name}.response -- | ServiceResponse | | ``` - Requests include a monotonically-increasing `request_id` for correlation - Multiple clients can call the same server concurrently - Each client filters responses by matching its `request_id` - Communication uses shared memory (zero-copy IPC) --- ## Error Handling ```rust match client.call(request, Duration::from_secs(1)) { Ok(response) => { /* success */ } Err(ServiceError::Timeout) => { /* server didn't respond in time */ } Err(ServiceError::ServiceFailed(msg)) => { /* handler returned Err */ } Err(ServiceError::NoServer) => { /* no server found */ } Err(ServiceError::Transport(msg)) => { /* IPC error */ } } ``` | Error | Cause | |-------|-------| | `Timeout` | Server didn't respond within the timeout duration | | `ServiceFailed(msg)` | Server handler returned `Err(msg)` | | `NoServer` | No service server is running | | `Transport(msg)` | IPC/shared memory communication failure | --- ## Complete Example A service that looks up robot joint limits by name: ```rust use horus::prelude::*; use std::collections::HashMap; // Define the service service! { GetJointLimits { request { joint_name: String, } response { min_position: f64, max_position: f64, max_velocity: f64, max_effort: f64, } } } fn main() -> Result<()> { // Joint limits database let limits: HashMap = HashMap::from([ ("shoulder".into(), (-3.14, 3.14, 2.0, 100.0)), ("elbow".into(), (0.0, 2.61, 2.0, 80.0)), ("wrist".into(), (-1.57, 1.57, 3.0, 40.0)), ]); // Start server let _server = ServiceServerBuilder::::new() .on_request(move |req| { match limits.get(&req.joint_name) { Some(&(min, max, vel, effort)) => Ok(GetJointLimitsResponse { min_position: min, max_position: max, max_velocity: vel, max_effort: effort, }), None => Err(format!("Unknown joint: {}", req.joint_name)), } }) .build()?; // Client usage let mut client = ServiceClient::::new()?; let resp = client.call( GetJointLimitsRequest { joint_name: "elbow".into() }, Duration::from_secs(1), )?; println!("Elbow limits: [{:.2}, {:.2}] rad", resp.min_position, resp.max_position); Ok(()) } ``` --- ## CLI Commands ```bash # List active services horus service list ``` --- ## Next Steps - **[Services API Reference](/rust/api/services)** — Full method tables for ServiceClient, AsyncServiceClient, ServiceServerBuilder - **[Communication Overview](/concepts/communication-overview)** — When to use topics vs services vs actions - **[Actions](/concepts/actions)** — Long-running tasks with feedback - **[Topic](/concepts/core-concepts-topic)** — Pub/sub messaging --- ## Multi-Process Architecture Path: /concepts/multi-process Description: Run HORUS nodes across separate processes — shared memory auto-discovery, mixed languages, process isolation # Multi-Process Architecture HORUS topics work transparently across process boundaries. Two nodes in separate processes communicate the same way as two nodes in the same process — through shared memory. No broker, no serialization layer, no configuration. ```rust // Process 1: sensor.rs let topic: Topic = Topic::new("imu")?; topic.send(imu_reading); // Process 2: controller.rs let topic: Topic = Topic::new("imu")?; // same name = same topic if let Some(reading) = topic.recv() { // Got it — zero-config, sub-microsecond } ``` --- ## How It Works When you call `Topic::new("imu")`, HORUS creates (or opens) a shared memory region. Any process on the same machine that calls `Topic::new("imu")` with the same type connects to the same underlying ring buffer. The shared memory backend is managed by `horus_sys` — you never configure paths manually. HORUS auto-detects whether a topic is same-process or cross-process and picks the fastest path: | Scenario | Latency | How It Works | |----------|---------|--------------| | Same thread | ~3ns | Direct pointer handoff | | Same process, 1:1 | ~18ns | Lock-free single-producer/single-consumer ring buffer | | Same process, 1:N | ~24ns | Broadcast to multiple in-process subscribers | | Same process, N:1 | ~26ns | Multiple in-process publishers, one subscriber | | Same process, N:N | ~36ns | Full many-to-many in-process | | Cross-process, POD type | ~50ns | Zero-copy shared memory (no serialization) | | Cross-process, N:1 | ~65ns | Shared memory, multiple publishers | | Cross-process, 1:N | ~70ns | Shared memory, multiple subscribers | | Cross-process, 1:1 | ~85ns | Shared memory, serialized type | | Cross-process, N:N | ~167ns | Shared memory, full many-to-many | Cross-process adds ~30-130ns vs in-process — still sub-microsecond. You don't configure any of this. The backend is selected automatically based on topology and upgrades transparently as participants join or leave. --- ## Running Multiple Processes ### Option 1: `horus run` with Multiple Files ```bash # Builds and runs both files as separate processes horus run sensor.rs controller.rs # Mixed languages work too horus run sensor.py controller.rs # With release optimizations horus run -r sensor.rs controller.rs ``` `horus run` compiles each file, then launches all processes and manages their lifecycle (SIGTERM on Ctrl+C, etc.). ### Option 2: Separate Terminals Run each node in its own terminal: ```bash # Terminal 1 horus run sensor.rs # Terminal 2 horus run controller.rs ``` Topics auto-discover via shared memory. No coordination needed. ### Option 3: `horus launch` (YAML) For production, declare your multi-process layout in a launch file: ```yaml # launch.yaml nodes: - name: sensor cmd: horus run sensor.rs - name: controller cmd: horus run controller.rs - name: monitor cmd: horus run monitor.py ``` ```bash horus launch launch.yaml ``` --- ## Example: Two-Process Sensor Pipeline **Process 1** — `sensor.rs`: ```rust use horus::prelude::*; message! { WheelEncoder { left_ticks: i64, right_ticks: i64, timestamp_ns: u64, } } struct EncoderNode { publisher: Topic, ticks: i64, } impl EncoderNode { fn new() -> Result { Ok(Self { publisher: Topic::new("wheel.encoders")?, ticks: 0, }) } } impl Node for EncoderNode { fn name(&self) -> &str { "Encoder" } fn tick(&mut self) { self.ticks += 10; self.publisher.send(WheelEncoder { left_ticks: self.ticks, right_ticks: self.ticks + 2, timestamp_ns: horus::now_ns(), }); } } fn main() -> Result<()> { let mut sched = Scheduler::new().tick_rate(100_u64.hz()); sched.add(EncoderNode::new()?).order(0).build()?; sched.run()?; Ok(()) } ``` **Process 2** — `odometry.rs`: ```rust use horus::prelude::*; message! { WheelEncoder { left_ticks: i64, right_ticks: i64, timestamp_ns: u64, } } struct OdometryNode { encoder_sub: Topic, odom_pub: Topic, last_left: i64, last_right: i64, } impl OdometryNode { fn new() -> Result { Ok(Self { encoder_sub: Topic::new("wheel.encoders")?, odom_pub: Topic::new("odom")?, last_left: 0, last_right: 0, }) } } impl Node for OdometryNode { fn name(&self) -> &str { "Odometry" } fn tick(&mut self) { if let Some(enc) = self.encoder_sub.recv() { let dl = enc.left_ticks - self.last_left; let dr = enc.right_ticks - self.last_right; self.last_left = enc.left_ticks; self.last_right = enc.right_ticks; println!("[Odom] delta L={} R={}", dl, dr); } } } fn main() -> Result<()> { let mut sched = Scheduler::new().tick_rate(100_u64.hz()); sched.add(OdometryNode::new()?).order(0).build()?; sched.run()?; Ok(()) } ``` Run them: ```bash # Terminal 1 horus run sensor.rs # Terminal 2 horus run odometry.rs ``` The `WheelEncoder` messages flow through shared memory at ~50ns latency, with zero configuration. --- ## When to Use Multi-Process | Factor | Single Process | Multi-Process | |--------|----------------|---------------| | **Latency** | ~3-36ns (intra-process) | ~50-167ns (cross-process) | | **Determinism** | Full control via scheduler ordering | Each process has its own scheduler | | **Isolation** | A crash takes down everything | A crash is contained to one process | | **Languages** | Single language per binary | Mix Rust + Python freely | | **Restart** | Must restart everything | Restart one process independently | | **Debugging** | Single debugger session | Attach debugger to one process | | **Deployment** | One binary to deploy | Multiple binaries | | **Complexity** | Simpler | More moving parts | **Use single-process when:** - All nodes are the same language - You need deterministic ordering between nodes (e.g., sensor → controller → actuator) - Latency matters at the nanosecond level - Simpler deployment is preferred **Use multi-process when:** - Mixing Rust and Python (e.g., Rust motor control + Python ML inference) - Process isolation is needed (safety-critical separation) - Independent restart required (update one node without stopping others) - Different update rates or lifecycle requirements --- ## Introspection HORUS CLI tools work across processes automatically: ```bash # See all topics (from any process) horus topic list # Monitor a topic published by another process horus topic echo wheel.encoders # See all running nodes across processes horus node list # Check bandwidth across processes horus topic bw wheel.encoders ``` --- ## Cleaning Up Shared memory files persist after processes exit. Clean them with: ```bash horus clean --shm # Remove stale shared memory regions ``` Or they are automatically cleaned on the next `horus run`. --- ## See Also - [Topics (Full Reference)](/concepts/core-concepts-topic) — topic API and backend details - [Message Performance](/concepts/core-concepts-podtopic) — POD types and zero-copy transport - [Multi-Language](/concepts/multi-language) — Rust + Python interop patterns --- ## Multi-Language Support Path: /concepts/multi-language Description: Use HORUS with Python and Rust # Multi-Language Support HORUS supports multiple programming languages, allowing you to choose the best tool for each component of your robotics system. ## Supported Languages ### Rust (Native) **Best for:** High-performance nodes, control loops, real-time systems HORUS is written in Rust and provides the most complete API. All examples in the documentation use Rust by default. **Getting Started:** ```bash horus new my-project # Select: Rust (option 2) ``` **Learn more:** [Quick Start](/getting-started/quick-start) ### Python > **Production-Ready**: Full-featured Python API with advanced capabilities **Best for:** Rapid prototyping, AI/ML integration, data processing, visualization Python bindings (via PyO3) provide a simple, Pythonic API for HORUS with production-grade features: - **Per-node rate control** - Different rates for different nodes (100Hz sensor, 10Hz logger) - **Message timestamps** - Typed messages include `timestamp_ns` for timing - **Typed messages** - Type-safe messages shared with Rust (CmdVel, Pose2D, Imu, Odometry, LaserScan) - **Generic messages** - Send any Python type (dicts, lists, etc.) between Python nodes - **Multiprocess support** - Process isolation via shared memory Perfect for integrating with NumPy, PyTorch, TensorFlow, and other Python libraries. **Getting Started:** ```bash horus new my-project # Select: Python (option 1) ``` **Learn more:** [Python Bindings](/python/api/python-bindings) ## Cross-Language Communication Python and Rust nodes communicate through HORUS's shared memory system. For cross-language communication, both sides **must use the same typed message types**. ### Typed Topics for Cross-Language Communication Pass a message type class to the Python `Topic()` constructor to create a typed topic that Rust can read: ```python # Python publisher (pose_publisher.py) from horus import Topic, Pose2D # Create typed topic - the type determines the topic name and serialization topic = Topic(Pose2D) # Send a Pose2D message (shared memory, readable by Rust) pose = Pose2D(x=1.0, y=2.0, theta=0.5) topic.send(pose) ``` ```rust // Rust subscriber (in another process) use horus::prelude::*; let topic: Topic = Topic::new("pose")?; if let Some(pose) = topic.recv() { println!("Received: x={}, y={}, theta={}", pose.x, pose.y, pose.theta); } ``` ### Supported Typed Message Types These types work for Python-Rust cross-language communication: | Message Type | Python Constructor | Default Topic Name | Use Case | |-------------|-------------------|-------------------|----------| | `CmdVel` | `CmdVel(linear, angular)` | `cmd_vel` | Velocity commands | | `Pose2D` | `Pose2D(x, y, theta)` | `pose` | 2D position | | `Imu` | `Imu(accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z)` | `imu` | IMU sensor data | | `Odometry` | `Odometry(x, y, theta, linear_velocity, angular_velocity)` | `odom` | Odometry data | | `LaserScan` | `LaserScan(angle_min, angle_max, ..., ranges=[...])` | `scan` | LiDAR scans | All message types include an optional `timestamp_ns` field for nanosecond timestamps. **Usage examples:** ```python from horus import Topic, CmdVel, Imu, LaserScan # Velocity commands cmd_topic = Topic(CmdVel) cmd_topic.send(CmdVel(linear=1.5, angular=0.3)) # IMU data imu_topic = Topic(Imu) imu_topic.send(Imu( accel_x=0.0, accel_y=0.0, accel_z=9.81, gyro_x=0.0, gyro_y=0.0, gyro_z=0.1 )) # Receive (returns typed Python object or None) if cmd := cmd_topic.recv(): print(f"linear={cmd.linear}, angular={cmd.angular}") ``` ### Generic Topics (Python-to-Python Only) For Python-to-Python communication, pass a string name to create a generic topic that can send any Python type: ```python from horus import Topic # Generic topic - pass topic name as string topic = Topic("my_data") topic.send({"sensor": "lidar", "ranges": [1.0, 1.1, 1.2]}) topic.send([1, 2, 3]) topic.send("hello") # Receive if msg := topic.recv(): print(msg) # Python dict, list, string, etc. ``` Generic topics use JSON/MessagePack serialization internally. **Rust nodes cannot read generic topics** — use typed messages for cross-language communication. **When to use which:** - **Typed Topics** (`Topic(CmdVel)`, `Topic(Pose2D)`) — Cross-language Rust+Python or Python-only - **Generic Topics** (`Topic("topic_name")`) — Python-only systems with custom data ## Python Node API The Python `Node` class provides a simple callback-based API: ```python import horus from horus import CmdVel, Pose2D, Topic pose_sub = Topic(Pose2D) cmd_pub = Topic(CmdVel) def controller_tick(node): pose = pose_sub.recv() if pose is not None: # Compute velocity command from pose cmd = CmdVel(linear=1.0, angular=0.5) cmd_pub.send(cmd) controller = horus.Node(name="Controller", tick=controller_tick, order=0, rate=30, subs=["pose"], pubs=["cmd_vel"]) horus.run(controller) ``` **Key Topic methods:** - `topic.send(msg)` — Send data to a topic - `topic.recv()` — Get next message (returns `None` if no messages) ## Choosing a Language | Use Case | Recommended Language | |----------|---------------------| | **Control loops** | Rust (lowest latency) | | **AI/ML models** | Python (ecosystem) | | **Hardware drivers** | Rust | | **Data processing** | Python or Rust | | **Real-time systems** | Rust | | **Prototyping** | Python (fastest development) | ## Mixed-Language Systems You can build systems with nodes in different languages: **Example: Robot with mixed languages** - **Motor controller** (Rust) — 1kHz control loop - **Vision processing** (Python) — PyTorch object detection - **Hardware driver** (Rust) — Sensor integration - **Monitor** (Rust) — Real-time visualization All communicate through HORUS shared memory with **sub-microsecond latency**. ### Running Mixed-Language Systems The `horus run` command automatically handles compilation and execution of mixed-language systems: ```bash # Mix Python and Rust nodes horus run sensor.py controller.rs visualizer.py # Mix Rust and Python horus run lidar_driver.rs planner.py motor_control.rs ``` **What happens:** 1. **Rust files** (`.rs`) are automatically compiled with `cargo build` using HORUS dependencies 2. **Python files** (`.py`) are executed directly with Python 3 3. All processes communicate via shared memory (managed automatically by `horus_sys`) 4. `horus run` manages the lifecycle (start, monitor, stop all together) **Note:** For Rust files, `horus run` creates a temporary Cargo project in `.horus/` with proper dependencies, builds it with `cargo build`, and executes the resulting binary. ### Example: Complete Mixed System **Python sensor node** (`sensor.py`): ```python import horus from horus import LaserScan, Topic scan_pub = Topic(LaserScan) def lidar_tick(node): scan = LaserScan( angle_min=-1.57, angle_max=1.57, angle_increment=0.01, range_min=0.1, range_max=10.0, ranges=[1.0, 1.1, 1.2, 0.9] ) scan_pub.send(scan) sensor = horus.Node(name="LidarSensor", tick=lidar_tick, order=0, rate=10, pubs=["scan"]) horus.run(sensor) ``` **Rust planner node** (`planner.rs`): ```rust use horus::prelude::*; fn main() -> Result<()> { let scan_topic: Topic = Topic::new("scan")?; let cmd_topic: Topic = Topic::new("cmd_vel")?; loop { if let Some(scan) = scan_topic.recv() { let cmd = plan_path(&scan); // Your planning logic cmd_topic.send(cmd); } } } ``` ```bash # Run both together horus run sensor.py planner.rs # Both processes communicate via shared memory ``` **Benefits:** - **No manual compilation** — `horus run` handles it - **Automatic dependency management** — HORUS libraries linked correctly - **Process isolation** — One crash doesn't kill the whole system - **True parallelism** — Each process can use separate CPU cores ## API Parity | Feature | Rust | Python | |---------|------|--------| | **Topic send/recv** | `topic.send(msg)` / `topic.recv()` | `topic.send(msg)` / `topic.recv()` | | **Typed messages** | `Topic` | `Topic(CmdVel)` | | **Generic messages** | `Topic` | `Topic("name")` | | **Node lifecycle** | `init()`, `tick()`, `shutdown()` | `init()`, `tick()`, `shutdown()` callbacks | | **Scheduler** | `Scheduler::new()` | `Scheduler()` | | **Node priority** | `.order(n)` | `order=n` | | **Rate control** | `Scheduler` rate | `rate=Hz` per node | | **Transport selection** | Automatic (topology-based) | Automatic (topology-based) | | **Message types** | Full horus_library | CmdVel, Pose2D, Imu, Odometry, LaserScan + horus.library | | **TransformFrame transforms** | `TransformFrame::new()` | `TransformFrame()` | | **Tensor system** | Native | `Image`, `PointCloud`, `DepthImage` (pool-backed) | | **Logging** | `hlog!(info, ...)` | `node.log_info(...)` | ## Next Steps **Choose your language:** - [Python Bindings](/python/api/python-bindings) — Full guide with examples - [Quick Start](/getting-started/quick-start) — Get started with Rust **Build something:** - [Examples](/rust/examples/basic-examples) — See multi-language systems in action - [CLI Reference](/development/cli-reference) — `horus new` command options --- ## Communication Patterns Overview Path: /concepts/communication-overview Description: Understanding HORUS communication primitives — Topics and Actions # Communication Patterns Overview HORUS provides three communication primitives that cover all robotics messaging needs: | Pattern | Purpose | Example | |---------|---------|---------| | **Topic** | Streaming data (pub/sub) | Sensor readings, motor commands, video frames | | **Service** | Request/response RPC | Parameter queries, configuration, one-shot commands | | **Action** | Long-running tasks (goal/feedback/result) | Navigation, arm motion, calibration | Both work locally (shared memory) and across machines (network transport via configuration). ## Topic: Streaming Pub/Sub `Topic` is the primary communication primitive. It provides ultra-fast pub/sub with automatic optimization — **~3ns to ~167ns** latency depending on topology. ```rust use horus::prelude::*; let pub_topic: Topic = Topic::new("sensor.temperature")?; pub_topic.send(25.3); let sub_topic: Topic = Topic::new("sensor.temperature")?; if let Some(temp) = sub_topic.recv() { hlog!(info, "Temperature: {}", temp); } ``` | Scenario | Latency | |----------|---------| | Same thread | ~3ns | | Same process | ~18-36ns | | Cross-process | ~50-167ns | The backend is auto-selected based on topology and participant count. Communication paths upgrade transparently at runtime as participants join. For full Topic API details, see [Topic Communication](/concepts/core-concepts-topic). ## Action: Long-Running Tasks Actions handle tasks that take time to complete and benefit from progress feedback and cancellation. They follow the **Goal / Feedback / Result** pattern. For full Action API details, see [Actions](/concepts/actions). ## When to Use What | Scenario | Use | Why | |----------|-----|-----| | Sensor data streaming | **Topic** | Continuous data, latest value matters | | Motor commands | **Topic** | High frequency, low latency | | Camera frames | **Topic** | Streaming, multiple consumers | | Navigate to position | **Action** | Long-running, needs progress feedback | | Pick-and-place | **Action** | Multi-step, cancellable | | Calibration routine | **Action** | Takes time, reports progress | | Emergency stop | **Topic** | Must be instantaneous, no handshake | | Diagnostics broadcast | **Topic** | Many publishers, flexible topology | **Rule of thumb**: If it's a continuous stream of data, use **Topic**. If it's a task you'd want to start, monitor, and potentially cancel, use **Action**. ## Composing Patterns A typical robot system combines both patterns: ```rust struct NavigationSystem { // Topics for streaming sensor data lidar_sub: Topic, odom_sub: Topic, // Topics for publishing commands cmd_vel_pub: Topic, status_pub: Topic, // Action server for goal-based navigation nav_server: ActionServerNode, } ``` The navigation action server subscribes to sensor topics internally, computes a path, publishes velocity commands via topics, and reports progress back to the action client — all using the same underlying shared memory infrastructure. ## Next Steps - [Topic Details](/concepts/core-concepts-topic) - Architecture and design patterns - [Topic API Reference](/rust/api/topic) - Full method documentation - [Actions](/concepts/actions) - Goal/feedback/result pattern - [Services](/concepts/services) - Synchronous request/response RPC --- ## Architecture Overview Path: /concepts/architecture Description: Understanding HORUS - the node model, communication patterns, scheduling, and memory system # Architecture Overview HORUS is built on four foundational concepts that work together to create a high-performance robotics runtime: Nodes
Computational units"] C["Communication
Data exchange"] S["Scheduler
Orchestration"] M["Memory
Zero-copy sharing"] end N <--> C C <--> S S <--> M M <--> N `} caption="The Four Pillars of HORUS" /> --- ## The Node Model Everything in HORUS is a **Node**. A node is an independent unit of computation with a well-defined lifecycle. ### Why Nodes? Robotics systems are inherently modular. A robot has sensors, actuators, planners, and controllers - each with different timing requirements and failure modes. By making each component a node, HORUS provides: - **Isolation** - A failing camera driver doesn't crash your motion controller - **Composability** - Mix and match nodes to build different robots - **Testability** - Test each node independently before integration - **Reusability** - Share nodes across projects via the package registry ### Node Lifecycle Every node follows the same lifecycle, ensuring predictable behavior: I["Initializing"] I --> R["Running"] R --> S["Stopping"] S --> D["Stopped"] R -.-> E["Error"] E --> R E --> S R -.-> C["Crashed"] `} caption="Node State Machine" /> | State | What Happens | |-------|--------------| | **Uninitialized** | Node exists but hasn't started | | **Initializing** | Setting up resources, connecting to hardware | | **Running** | Actively processing - `tick()` called each cycle | | **Stopping** | Cleaning up, releasing resources | | **Stopped** | Fully shut down | | **Error(msg)** | Something went wrong, but recoverable | | **Crashed(msg)** | Unrecoverable failure | ### The Tick Model Nodes don't run continuously - they **tick**. Each tick is a discrete unit of work: ```rust fn tick(&mut self) { // Read inputs if let Some(sensor_data) = self.sensor_topic.recv() { // Process let command = self.compute_response(sensor_data); // Write outputs self.command_topic.send(command); } } ``` This model enables: - **Deterministic timing** - Know exactly when each node runs - **Profiling** - Measure how long each tick takes - **Scheduling intelligence** - The scheduler can optimize execution order --- ## Communication Nodes need to exchange data. HORUS provides a single **Topic** API that automatically selects the optimal backend based on how many nodes are communicating and whether they're in the same process. ### Topic: One API, Automatic Optimization You always use the same `Topic::new()` call. HORUS automatically detects the topology and selects the fastest communication path — from **~3ns** (same-thread) to **~167ns** (cross-process, many-to-many): ```rust // Same API for all communication patterns let topic: Topic = Topic::new("camera.image")?; topic.send(&frame); // Another node subscribes — same API let topic: Topic = Topic::new("camera.image")?; if let Some(frame) = topic.recv() { // Process frame } ``` No configuration needed — the backend is selected and upgraded transparently as participants join or leave. ### Cross-Process Communication Topics work transparently across process boundaries using shared memory: |"publish"| T -->|"subscribe"| N2 `} caption="Transparent Cross-Process Communication" /> Data goes through shared memory with sub-microsecond latency. Simple fixed-size types get an even faster zero-copy path automatically. --- ## The Scheduler The scheduler is the brain of HORUS. It decides **when** and **how** nodes execute. ### Why a Scheduler? Without coordination, nodes would: - Fight for CPU resources - Miss real-time deadlines - Waste cycles waiting for data that hasn't arrived The HORUS scheduler solves these problems with intelligent orchestration. ### Execution Modes Different applications need different scheduling strategies: Scheduler
Choose execution mode"] SCHED --> PAR["Parallel
Multi-core"] SCHED --> SEQ["Sequential
Simple & predictable"] `} caption="Execution Modes" /> | Mode | Best For | Tick Overhead | |------|----------|---------------| | **Sequential** | Safety-critical, debugging | Minimal | | **Parallel** | CPU-heavy workloads | Varies by core count | ### Profiling The scheduler tracks node execution statistics for diagnostics and optimization: - **Runtime Profiler** - Tracks how long each node takes (avg, min, max tick duration) - **NodeMetrics** - Per-node metrics: total ticks, successful ticks, failed ticks, messages sent/received ### Safety Systems Real robots need safety guarantees. The scheduler provides: | Feature | Purpose | |---------|---------| | **WCET Monitoring** | Detect nodes exceeding time budgets | | **Fault Tolerance** | Isolate failing nodes automatically | | **Watchdog Timers** | Detect hung nodes | | **Black Box** | Flight recorder for post-mortem analysis | --- ## Memory System Large data (images, point clouds, ML tensors) needs special handling. Copying a 4K image between nodes would destroy performance. ### Zero-Copy Design HORUS uses shared memory pools for large data: 1920x1080x3"] end CAM["Camera"] -->|"write once"| T T -->|"read"| V1["Vision 1"] T -->|"read"| V2["Vision 2"] T -->|"read"| V3["Vision 3"] `} caption="Zero-Copy: Write Once, Read Many" /> The image data is written **once** to shared memory. Each subscriber reads directly from the same memory location - no copying. ### Zero-Copy Large Data Large data types like `Image`, `PointCloud`, and `DepthImage` are backed by shared memory automatically: ```rust let mut img = Image::new(1920, 1080, ImageEncoding::Rgb8)?; camera.capture_into(img.data_mut()); let topic: Topic = Topic::new("camera.rgb")?; topic.send(&img); // Zero-copy — only a descriptor crosses the ring buffer ``` **Zero-copy characteristics:** - Fast allocation (~100ns) - Automatic reference counting - Works across processes ### Python Integration Python nodes share the same memory pool: ```python import horus import numpy as np # Receive tensor from Rust node tensor = topic.recv() # Zero-copy numpy view - no data copied! array = np.array(tensor, copy=False) # Process with numpy/PyTorch result = model.predict(array) ``` --- ## Data Flow Example Here's how these concepts work together in a typical perception-to-action pipeline: Camera
Captures frames"] DET["Detector
Finds objects"] NAV["Planner
Plans path"] CTRL["Controller
Computes motion"] MOT["Motors
Executes movement"] CAM -->|"Image
(zero-copy)"| DET DET -->|"Detections
(Topic)"| NAV NAV -->|"Velocity
(Topic)"| CTRL CTRL -->|"PWM
(Topic)"| MOT `} caption="Perception-Planning-Control Pipeline" /> | Connection | Mechanism | Why | |------------|-----------|-----| | Camera → Detector | Image (zero-copy) | Large image, shared memory | | Detector → Planner | Topic | Multiple planners might subscribe | | Planner → Controller | Topic | Monitoring tools can observe | | Controller → Motors | Topic | Direct pipeline connection | **Total pipeline latency:** Under 1 microsecond for message passing (same-process backends). --- ## Performance Summary | Metric | Value | |--------|-------| | Same-thread topic | ~3 ns | | Same-process topic | ~18-36 ns | | Cross-process topic | ~50-167 ns | | Scheduler tick overhead | ~50-100ns | | Shared memory allocation | ~100ns | --- ## Design Philosophy HORUS is built on these principles: 1. **Nodes are the unit of composition** - Build robots by connecting nodes 2. **Communication is explicit** - No hidden data flow, everything goes through Topic 3. **The scheduler is your friend** - Let it optimize; don't fight it 4. **Zero-copy by default** - Large data should never be copied unnecessarily 5. **Safety is not optional** - Fault tolerance, watchdogs, and black boxes are built in --- ## Next Steps - **[Quick Start](/getting-started/quick-start)** - Build your first HORUS application - **[Core Concepts: Nodes](/concepts/core-concepts-nodes)** - Deep dive into the node model - **[Core Concepts: Topic](/concepts/core-concepts-topic)** - Advanced pub/sub patterns - **[Scheduler Configuration](/advanced/scheduler-configuration)** - Tuning for real-time --- ## Custom Messages & Performance Path: /concepts/core-concepts-podtopic Description: Define your own message types and understand how HORUS optimizes transfer speed # Custom Messages & Performance Need a message type that doesn't exist in the standard library? Use the `message!` macro. HORUS handles all the optimization automatically — you never need to think about serialization or memory layout. For performance-sensitive applications: standard fixed-size types transfer at **~50ns** (zero-copy), variable-size types at **~167ns** (serialized). No configuration needed. ## Automatic Optimization When you create a `Topic`, HORUS inspects the message type at compile time and selects the fastest transfer strategy: - **Fixed-size types** (no heap allocations) use raw memory copy — no serialization overhead - **Variable-size types** (containing `String`, `Vec`, etc.) use fast bincode serialization You always use the same API. The optimization is invisible: ```rust use horus::prelude::*; // Fixed-size type — automatically uses zero-copy (~50ns cross-process) let cmd_topic: Topic = Topic::new("cmd_vel")?; cmd_topic.send(CmdVel::new(1.0, 0.5)); // Variable-size type — automatically uses serialization (~167ns cross-process) let log_topic: Topic = Topic::new("log")?; log_topic.send("Motor started".to_string()); ``` ## Performance Comparison | Type Category | Cross-Process Latency | Examples | |---------------|----------------------|----------| | Fixed-size (zero-copy) | ~50-85ns | `CmdVel`, `Imu`, `Pose2D`, `Heartbeat` | | Variable-size (serialized) | ~167ns | `String`, `Vec`, `HashMap` | Same-process communication is fast for both categories since no shared memory serialization is needed. For most applications, the ~167ns serialized path is more than fast enough. Only high-frequency control loops running at 1kHz+ benefit noticeably from the zero-copy path. ## Custom Messages Use the `message!` macro to define your own message types. The macro handles all optimization traits automatically: ```rust use horus::prelude::*; message! { /// Motor feedback data sent at 1kHz pub struct MotorFeedback { pub timestamp_ns: u64, pub motor_id: u32, pub velocity: f32, pub current_amps: f32, pub temperature_c: f32, } } // HORUS automatically selects the fastest transfer path let feedback: Topic = Topic::new("motor.feedback")?; feedback.send(MotorFeedback { timestamp_ns: 0, motor_id: 1, velocity: 3.14, current_amps: 0.5, temperature_c: 45.0, }); ``` The `message!` macro works for both fixed-size and variable-size types — HORUS selects the right transfer strategy automatically. ## Built-in Message Types All standard HORUS message types are pre-optimized. Fixed-size types automatically use the zero-copy fast path. ### Geometry | Message | Description | |---------|-------------| | `CmdVel` | 2D velocity command (linear + angular) | | `Pose2D` | 2D position and orientation | | `Twist` | 3D linear and angular velocity | | `TransformStamped` | 3D transformation with timestamp | | `Point3` | 3D point | | `Vector3` | 3D vector | | `Quaternion` | Rotation quaternion | ### Sensors | Message | Description | |---------|-------------| | `Imu` | Inertial measurement unit data | | `LaserScan` | 2D laser range data | | `Odometry` | Position/velocity estimate | | `RangeSensor` | Single distance measurement | | `BatteryState` | Battery level and status | | `NavSatFix` | GPS position | ### Control | Message | Description | |---------|-------------| | `MotorCommand` | Individual motor control | | `DifferentialDriveCommand` | Differential drive control | | `ServoCommand` | Servo position/velocity | | `JointCommand` | Joint-level control | | `TrajectoryPoint` | Trajectory waypoint | | `PidConfig` | PID controller parameters | ### Diagnostics | Message | Description | |---------|-------------| | `Heartbeat` | Liveness signal | | `NodeHeartbeat` | Per-node health status | | `DiagnosticStatus` | General status report | | `EmergencyStop` | Emergency stop signal | | `SafetyStatus` | Safety system state | | `ResourceUsage` | CPU/memory usage | | `DiagnosticValue` | Single diagnostic measurement | | `DiagnosticReport` | Full diagnostic report | ### Navigation | Message | Description | |---------|-------------| | `NavGoal` | Navigation goal | | `GoalResult` | Goal completion result | | `Waypoint` | Navigation waypoint | | `NavPath` | Sequence of waypoints | | `PathPlan` | Planned path | | `VelocityObstacle` | Velocity obstacle for avoidance | | `VelocityObstacles` | Set of velocity obstacles | ### Force/Haptics | Message | Description | |---------|-------------| | `WrenchStamped` | Force/torque measurement | | `ForceCommand` | Force control command | | `ImpedanceParameters` | Impedance control config | | `ContactInfo` | Contact detection data | | `HapticFeedback` | Haptic output command | ### Input | Message | Description | |---------|-------------| | `JoystickInput` | Gamepad/joystick state | | `KeyboardInput` | Keyboard key events | ### Tensor | Message | Description | |---------|-------------| | `Tensor` | Fixed-size tensor descriptor | ```rust use horus::prelude::*; // All built-in messages use the fastest available path automatically let cmd: Topic = Topic::new("cmd_vel")?; let pose: Topic = Topic::new("robot_pose")?; let imu: Topic = Topic::new("imu_data")?; let estop: Topic = Topic::new("emergency_stop")?; ``` ## See Also - **[Topic](/concepts/core-concepts-topic)** — The unified communication API - **[Message Types](/concepts/message-types)** — Full message type reference - **[Architecture](/concepts/architecture)** — How communication fits into the HORUS architecture --- ## TransformFrame Transform System Path: /concepts/transform-frame Description: High-performance coordinate frame management for real-time robotics # TransformFrame Transform System TransformFrame is HORUS's coordinate frame management system — a real-time-safe replacement for ROS2 TF2. It manages the spatial relationships between coordinate frames (e.g., "where is the camera relative to the robot base?") with lock-free lookups and sub-microsecond performance. ## Why TransformFrame? | Problem with TF2 | TransformFrame Solution | |------------------|-----------------| | Mutex-based locking | Lock-free seqlock protocol | | Unbounded latency spikes | Predictable sub-microsecond latency | | String-only frame lookup | Dual API: Integer IDs + Names | | No hard real-time support | Real-time safe, no allocations in hot path | ### Performance | Operation | TransformFrame | ROS2 TF2 | Speedup | |-----------|--------|----------|---------| | Lookup by ID | ~50ns | N/A | - | | Lookup by name | ~200ns | ~2us | 10x | | Chain resolution (depth 3) | ~150ns | ~5us | 33x | | Chain resolution (depth 10) | ~2.5us | ~15us | 6x | | Concurrent reads (4 threads) | ~800ns | ~8us | 10x | --- ## Basic Usage ```rust use horus::prelude::*; // Provides TransformFrame, TransformFrameConfig, Transform, timestamp_now // Create with default config (256 frames) let hf = TransformFrame::new(); // Register frame hierarchy: world → base_link → camera hf.register_frame("world", None)?; hf.register_frame("base_link", Some("world"))?; hf.register_frame("camera", Some("base_link"))?; // Update transform (camera position relative to base_link) let tf = Transform::new( [0.1, 0.0, 0.3], // translation [x, y, z] in meters [0.0, 0.0, 0.0, 1.0], // quaternion [x, y, z, w] ); hf.update_transform("camera", &tf, timestamp_now())?; // Query: where is camera relative to world? let camera_to_world = hf.tf("camera", "world")?; let point_in_world = camera_to_world.transform_point([1.0, 0.0, 0.0]); ``` ## Transform Type ```rust pub struct Transform { pub translation: [f64; 3], // [x, y, z] in meters pub rotation: [f64; 4], // quaternion [x, y, z, w] (Hamilton convention) } ``` ### Creating Transforms ```rust // From translation + quaternion let tf = Transform::new([1.0, 2.0, 3.0], [0.0, 0.0, 0.0, 1.0]); // Translation only (identity rotation) let tf = Transform::from_translation([1.0, 0.0, 0.0]); // Rotation only (no translation) let tf = Transform::from_rotation([0.0, 0.0, 0.0, 1.0]); // From Euler angles (roll, pitch, yaw) let tf = Transform::from_euler([1.0, 0.0, 0.0], [0.1, 0.2, 0.3]); // From axis-angle (translation, axis, angle) let tf = Transform::from_axis_angle([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], std::f64::consts::PI / 2.0); // Identity transform let tf = Transform::identity(); ``` ### Transform Operations ```rust // Compose two transforms let composed = tf1.compose(&tf2); // Invert a transform let inverse = tf.inverse(); // Apply to a point (translation + rotation) let point_world = tf.transform_point([1.0, 0.0, 0.0]); // Apply to a vector (rotation only) let vector_world = tf.transform_vector([1.0, 0.0, 0.0]); // Interpolate between transforms (t=0.0 to 1.0) // Uses linear interpolation for translation, SLERP for rotation let tf_mid = tf1.interpolate(&tf2, 0.5); // Convert to/from 4x4 matrix let matrix = tf.to_matrix(); let tf = Transform::from_matrix(matrix); // Extract Euler angles [roll, pitch, yaw] let euler = tf.to_euler(); ``` --- ## Frame Registration ### Dynamic Frames Dynamic frames have transforms that change over time (e.g., robot joints, camera tracking): ```rust // Register with parent relationship let camera_id = hf.register_frame("camera", Some("base_link"))?; // Root frame (no parent) hf.register_frame("world", None)?; // Update transform over time hf.update_transform("camera", &new_tf, timestamp_now())?; // Or use cached frame ID for faster updates hf.update_transform_by_id(camera_id, &new_tf, timestamp_now()); ``` ### Static Frames Static frames have transforms that never change (e.g., sensor mounts, fixed offsets): ```rust // Register with initial transform let mount_tf = Transform::from_translation([0.1, 0.0, 0.2]); hf.register_static_frame("lidar_mount", Some("base_link"), &mount_tf)?; // Update static transform (rare, e.g., recalibration) hf.set_static_transform("lidar_mount", &new_tf)?; ``` Static frames use less memory since they don't maintain a history buffer. ### Frame Queries ```rust // Get frame ID (cache this for hot paths!) let id: Option = hf.frame_id("camera"); // Get frame name by ID let name: Option = hf.frame_name(id); // Check if frame exists if hf.has_frame("camera") { /* ... */ } // List all frames let all: Vec = hf.all_frames(); // Get parent/children let parent: Option = hf.parent("camera"); let children: Vec = hf.children("base_link"); ``` --- ## Transform Lookups ### By Name ```rust // Get transform from source frame to destination frame let tf = hf.tf("camera", "world")?; // Check if transform path exists if hf.can_transform("camera", "world") { let tf = hf.tf("camera", "world")?; } ``` ### By ID (Hot Path) For control loops running at 1kHz+, cache frame IDs and use the ID-based API: ```rust // Cache IDs once at startup let camera_id = hf.frame_id("camera").unwrap(); let world_id = hf.frame_id("world").unwrap(); // Use IDs in hot loop (~50ns vs ~200ns for name-based) loop { let tf = hf.tf_by_id(camera_id, world_id); if let Some(tf) = tf { let target_in_world = tf.transform_point(target_in_camera); // Control logic... } } ``` ### Time-Travel Queries TransformFrame maintains a history buffer of past transforms, enabling queries at past timestamps with interpolation: ```rust // Get transform at a specific past time let past = timestamp_now() - 100_000_000; // 100ms ago let tf = hf.tf_at("camera", "world", past)?; // ID-based version let tf = hf.tf_at_by_id(camera_id, world_id, past); ``` If the exact timestamp isn't available, TransformFrame interpolates between the two nearest samples: - **Translation**: Linear interpolation - **Rotation**: SLERP (Spherical Linear Interpolation) --- ## Configuration ### Presets ```rust // Default / Small (256 frames, ~550KB) let hf = TransformFrame::new(); // Same as TransformFrame::small() let hf = TransformFrame::small(); // Medium (1024 frames, ~2.2MB) let hf = TransformFrame::medium(); // Large (4096 frames, ~9MB) let hf = TransformFrame::large(); ``` | Preset | Frames | Static | History | Cache | Memory | |--------|--------|--------|---------|-------|--------| | `small()` | 256 | 128 | 32 | 64 | ~550KB | | `medium()` | 1024 | 512 | 32 | 128 | ~2.2MB | | `large()` | 4096 | 2048 | 32 | 256 | ~9MB | Additional presets for simulation: ```rust // Massive (16384 frames, ~35MB) - 100+ robot simulations let hf = TransformFrame::with_config(TransformFrameConfig::massive()); // Unlimited frames (HashMap overflow for dynamic environments) let hf = TransformFrame::with_config(TransformFrameConfig::unlimited()); ``` ### Custom Configuration ```rust let hf = TransformFrame::with_config(TransformFrameConfig { max_frames: 2048, // Total frames (16-65536) max_static_frames: 1024, // Static frames (less memory, faster) history_len: 64, // Past transforms per dynamic frame (4-256) enable_overflow: false, // Allow HashMap for unlimited frames chain_cache_size: 256, // LRU cache for chain lookups }); ``` Or use the builder: ```rust let hf = TransformFrame::with_config( TransformFrameConfig::custom() .max_frames(512) .history_len(64) .build()? ); ``` --- ## CLI Tools HORUS provides CLI equivalents of ROS2 tf2 tools: ```bash # List all coordinate frames horus tf list horus tf list --json # JSON output # Echo transform between frames (like ros2 run tf2_ros tf2_echo) horus tf echo camera base_link horus tf echo camera world --rate 10 # 10 Hz # Show frame tree (like ros2 run tf2_tools view_frames) horus tf tree # Get detailed info about a specific frame horus tf info camera # Check if transform exists between two frames horus tf can camera world # Monitor transform update rates horus tf hz ``` --- ## Statistics and Inspection ### TransformFrameStats Get system-wide statistics via `tf.stats()`: ```rust let stats = hf.stats(); println!("{}", stats.summary()); println!("Frames: {}/{}", stats.total_frames, stats.max_frames); println!("Tree depth: {}, Roots: {}", stats.tree_depth, stats.root_count); ``` | Field | Type | Description | |-------|------|-------------| | `total_frames` | `usize` | Total registered frames | | `static_frames` | `usize` | Frames that never change | | `dynamic_frames` | `usize` | Frames updated at runtime | | `max_frames` | `usize` | Maximum capacity | | `history_len` | `usize` | Transform history buffer size | | `tree_depth` | `usize` | Maximum depth of the frame tree | | `root_count` | `usize` | Number of root frames (no parent) | ### FrameInfo Get metadata for a specific frame via `tf.frame_info(name)`: ```rust if let Some(info) = hf.frame_info("camera_link") { println!("Frame: {}, Parent: {:?}, Static: {}", info.name, info.parent, info.is_static); } ``` | Field | Type | Description | |-------|------|-------------| | `name` | `String` | Frame name | | `id` | `FrameId` | Internal frame ID | | `parent` | `Option` | Parent frame name (`None` for root) | | `is_static` | `bool` | Whether this frame never changes | ### Diagnostics ```rust // Validate frame tree integrity hf.validate()?; ``` --- ## See Also - **[Message Types](/concepts/message-types)** — TransformStamped and other spatial messages - **[Architecture](/concepts/architecture)** — How TransformFrame fits into HORUS - **[Quick Start](/getting-started/quick-start)** — Get started with HORUS --- ## Core Concepts Path: /concepts Description: Fundamental HORUS concepts and architecture # Core Concepts Understanding the fundamental concepts behind HORUS. ## Overview - [What is HORUS?](/concepts/what-is-horus) - Introduction, goals, and use cases - [Architecture](/concepts/architecture) - System design and components ## Core Components - [Nodes](/concepts/core-concepts-nodes) - Computational units and lifecycle - [Nodes (Beginner)](/concepts/nodes-beginner) - Simplified introduction to nodes - [Node Macro](/concepts/node-macro) - Declarative node definition with `node!` - [Topic (Pub/Sub)](/concepts/core-concepts-topic) - Unified pub/sub communication - [Topics (Beginner)](/concepts/topics-beginner) - Simplified introduction to topics - [Message Performance](/concepts/core-concepts-podtopic) - Zero-copy message transport - [Scheduler](/concepts/core-concepts-scheduler) - Execution orchestration - [Scheduler (Beginner)](/concepts/scheduler-beginner) - Simplified introduction to the scheduler - [Execution Classes](/concepts/execution-classes) - Rt, Compute, Event, AsyncIo, BestEffort ## Messages & Data - [Message Types](/concepts/message-types) - Standard robotics message types (CmdVel, Imu, LaserScan, etc.) - [TransformFrame](/concepts/transform-frame) - Hierarchical coordinate transforms ## Communication - [Communication Overview](/concepts/communication-overview) - Topics vs Services vs Actions - [Services](/concepts/services) - Request/response RPC pattern - [Actions](/concepts/actions) - Long-running tasks with feedback and cancellation - [Multi-Process](/concepts/multi-process) - Cross-process shared memory communication ## Configuration & Advanced - [horus.toml](/concepts/horus-toml) - Project manifest and configuration - [Choosing Configuration](/concepts/choosing-configuration) - Configuration patterns - [Real-Time](/concepts/real-time) - RT auto-detection, budgets, deadlines - [Multi-Language Support](/concepts/multi-language) - Rust + Python integration ======================================== # SECTION: Tutorials ======================================== --- ## Tutorials Path: /tutorials Description: Step-by-step guides that teach robotics concepts by building real applications with HORUS # Tutorials Each tutorial builds a **complete working application**. Follow them in order — each one builds on concepts from the previous. ## Learning Path | # | Tutorial | What You'll Build | What You'll Learn | |---|---------|-------------------|-------------------| | 1 | [Quick Start](/getting-started/quick-start) | Temperature sensor + monitor | Node, Topic, Scheduler basics | | 2 | [Sensor Pipeline](/getting-started/second-application) | 3-node sensor → filter → display | Multi-node architecture, message flow | | 3 | [IMU Sensor Node](/tutorials/01-sensor-node) | IMU data publisher at 100Hz | Custom data types, `.hz()`, sensor patterns | | 4 | [Motor Controller](/tutorials/02-motor-controller) | Velocity → position controller | Subscribing, state management, safe shutdown | | 5 | [Full Robot Integration](/tutorials/03-full-robot) | 4-node robot system | Multi-rate scheduling, data fusion, monitoring | | 6 | [Custom Message Types](/tutorials/04-custom-messages) | Battery monitor with custom messages | message! macro, manual derives, GenericMessage | | 7 | [Hardware Drivers](/tutorials/05-hardware-drivers) | Servo bus + custom conveyor driver | horus.toml [drivers], DriverHandle, register_driver! | | 8 | [Real-Time Control](/tutorials/realtime-control) | Hard-RT motor + lidar + planner | .rate(), .budget(), .on_miss(), .failure_policy(), .compute() | | 9 | [Choosing Configuration](/concepts/choosing-configuration) | Progressive config levels | Rate, budget, deadline, safety | | 10 | [Common Mistakes](/getting-started/common-mistakes) | — | 13 pitfalls and how to avoid them | ## Python Tutorials The same progression, using the Python API: | # | Tutorial | What You'll Build | |---|---------|-------------------| | 1 | [Quick Start (Python)](/getting-started/quick-start-python) | Temperature sensor + monitor | | 2 | [IMU Sensor Node (Python)](/tutorials/01-sensor-node-python) | IMU data publisher at 100Hz | | 3 | [Motor Controller (Python)](/tutorials/02-motor-controller-python) | Velocity → position controller with safe shutdown | | 4 | [Full Robot System (Python)](/tutorials/03-full-robot-python) | 4-node robot with multi-rate scheduling | | 5 | [Custom Messages (Python)](/tutorials/04-custom-messages-python) | Typed messages, dicts, dataclasses | | 6 | [Hardware & Real-Time (Python)](/tutorials/05-hardware-and-rt-python) | Drivers, budgets, deadlines, production config | ## Prerequisites - HORUS installed ([Installation Guide](/getting-started/installation)) - Basic Rust or Python knowledge ([Choosing a Language](/getting-started/choosing-language)) --- ## Tutorial: Real-Time Control Path: /tutorials/realtime-control Description: Build a multi-rate robot controller with real-time scheduling, deadline enforcement, and safety policies # Tutorial: Real-Time Control This tutorial walks through building a multi-rate robot controller with hard real-time guarantees. Estimated time: 20 minutes. ## What You'll Build A robot controller with three nodes running at different rates: 1. **Motor Controller** at 1kHz — real-time, safety-critical 2. **LiDAR Driver** at 100Hz — real-time sensor processing 3. **Path Planner** at 10Hz — compute-heavy, runs on a thread pool ## What You'll Learn - `.rate()` — set node tick frequency (auto-detects RT) - `.budget()` and `.deadline()` — execution time constraints - `.on_miss()` — what happens when a deadline is missed - `.failure_policy()` — how the system degrades gracefully - `.compute()` — offload work to a parallel thread pool - `.prefer_rt()` — request real-time scheduling when available - `.cores()` — pin nodes to specific CPU cores ## Prerequisites - A working HORUS project (see [Quick Start](/getting-started/quick-start)) - Basic familiarity with the `Node` trait --- ## Step 1: Define Three Nodes Start with three basic nodes. All run as `BestEffort` on the main thread — no real-time yet. ```rust use horus::prelude::*; struct MotorController { cmd_sub: Topic, } impl Node for MotorController { fn name(&self) -> &str { "motor_ctrl" } fn tick(&mut self) { if let Some(cmd) = self.cmd_sub.recv() { // Apply velocity command to motors } } fn enter_safe_state(&mut self) { // Zero velocity, engage brakes } } struct LidarDriver { scan_pub: Topic, } impl Node for LidarDriver { fn name(&self) -> &str { "lidar" } fn tick(&mut self) { // Read from hardware, publish scan let scan = LaserScan::default(); self.scan_pub.send(scan); } } struct PathPlanner { scan_sub: Topic, cmd_pub: Topic, } impl Node for PathPlanner { fn name(&self) -> &str { "planner" } fn tick(&mut self) { if let Some(scan) = self.scan_sub.recv() { // Compute path, publish velocity command let cmd = CmdVel::default(); self.cmd_pub.send(cmd); } } } ``` At this point every node runs once per scheduler tick with no timing guarantees. That is fine for prototyping, but a real robot needs deterministic timing. ## Step 2: Add Rates Adding `.rate()` does three things automatically: 1. Sets the tick frequency 2. Derives a default budget (80% of the period) 3. Derives a default deadline (95% of the period) 4. Automatically marks the node as real-time ```rust use horus::prelude::*; let mut scheduler = Scheduler::new(); scheduler.add(MotorController::new()) .order(0) .rate(1000_u64.hz()) // 1kHz -> budget=800us, deadline=950us, Rt .build()?; scheduler.add(LidarDriver::new()) .order(10) .rate(100_u64.hz()) // 100Hz -> budget=8ms, deadline=9.5ms, Rt .build()?; scheduler.add(PathPlanner::new()) .order(50) .rate(10_u64.hz()) // 10Hz -> budget=80ms, deadline=95ms, Rt .build()?; ``` The `.order()` value controls execution priority within a single tick. Lower values run first, so the motor controller always executes before the planner. Notice that all three nodes are now `Rt`. That is correct for the motor and LiDAR, but the path planner does not need hard real-time. We will fix that in Step 4. ## Step 3: Add Safety Policies Real-time without safety policies is dangerous. If the motor controller misses a deadline, the arm could overshoot its target and collide with something. ### Deadline miss behavior The `Miss` enum controls what happens when a node exceeds its deadline: | Variant | Behavior | |---------|----------| | `Miss::Warn` | Log a warning, continue | | `Miss::Skip` | Drop the late tick, run next on schedule | | `Miss::SafeMode` | Call `enter_safe_state()` on the node | | `Miss::Stop` | Shut down the node entirely | ### Applying safety to the motor controller ```rust use horus::prelude::*; scheduler.add(MotorController::new()) .order(0) .rate(1000_u64.hz()) .budget(800.us()) .deadline(950.us()) .on_miss(Miss::SafeMode) .failure_policy(FailurePolicy::Isolate) .max_deadline_misses(3) .build()?; ``` This means: if the motor controller misses its 950us deadline, call `enter_safe_state()` (which zeros velocity and engages brakes). After 3 consecutive misses, isolate the node from the rest of the system. ### Applying safety to the LiDAR The LiDAR is less critical — a missed scan is not dangerous, just suboptimal: ```rust use horus::prelude::*; scheduler.add(LidarDriver::new()) .order(10) .rate(100_u64.hz()) .on_miss(Miss::Skip) .build()?; ``` Skipping a single 100Hz LiDAR scan is acceptable. The planner will use the previous scan data. ## Step 4: Move the Planner to Compute The path planner does not need hard real-time. It runs complex algorithms that benefit from parallel execution on a thread pool. Use `.compute()` to move it off the RT thread: ```rust use horus::prelude::*; scheduler.add(PathPlanner::new()) .order(50) .rate(10_u64.hz()) .compute() // Runs on parallel thread pool, not RT thread .on_miss(Miss::Warn) // Planning delays are logged, not critical .build()?; ``` With `.compute()`, the planner runs on a worker thread instead of the real-time thread. This prevents a slow planning cycle from blocking the 1kHz motor loop. ## Step 5: Add RT Scheduling and CPU Pinning For production deployments, you want real-time OS scheduling and CPU isolation. These features require appropriate OS permissions (e.g., `CAP_SYS_NICE` on Linux). ```rust use horus::prelude::*; scheduler.add(MotorController::new()) .order(0) .rate(1000_u64.hz()) .budget(800.us()) .deadline(950.us()) .on_miss(Miss::SafeMode) .failure_policy(FailurePolicy::Isolate) .max_deadline_misses(3) .prefer_rt() // Request SCHED_FIFO if available .cores(&[0]) // Pin to CPU 0 .build()?; ``` - `.prefer_rt()` requests real-time OS scheduling (SCHED_FIFO on Linux). If the system lacks permissions, it falls back gracefully instead of failing. - `.cores(&[0])` pins the node to CPU 0, preventing the OS from migrating it across cores (which causes cache misses and jitter). For the LiDAR, pin it to a different core to avoid contention: ```rust use horus::prelude::*; scheduler.add(LidarDriver::new()) .order(10) .rate(100_u64.hz()) .on_miss(Miss::Skip) .prefer_rt() .cores(&[1]) // Separate core from motor controller .build()?; ``` ## Step 6: Complete System Here is the full program with all nodes configured: ```rust use horus::prelude::*; fn main() -> Result<(), Box> { let mut scheduler = Scheduler::new(); // 1kHz motor controller — safety-critical, RT, pinned to core 0 scheduler.add(MotorController::new()) .order(0) .rate(1000_u64.hz()) .budget(800.us()) .deadline(950.us()) .on_miss(Miss::SafeMode) .failure_policy(FailurePolicy::Isolate) .max_deadline_misses(3) .prefer_rt() .cores(&[0]) .build()?; // 100Hz LiDAR — RT, pinned to core 1 scheduler.add(LidarDriver::new()) .order(10) .rate(100_u64.hz()) .on_miss(Miss::Skip) .prefer_rt() .cores(&[1]) .build()?; // 10Hz path planner — compute thread pool, best-effort timing scheduler.add(PathPlanner::new()) .order(50) .rate(10_u64.hz()) .compute() .on_miss(Miss::Warn) .build()?; // Enable verbose timing output scheduler.verbose(true); // Run until Ctrl+C scheduler.build()?.run()?; Ok(()) } ``` ## Key Takeaways - **Rate implies RT.** Calling `.rate()` automatically sets budget, deadline, and marks the node as real-time. Override with `.compute()` or `.budget()`/`.deadline()` as needed. - **Safety is explicit.** Always set `.on_miss()` and `.failure_policy()` for safety-critical nodes. The defaults (`Miss::Warn`, no isolation) are intentionally permissive for development. - **Separate concerns by execution class.** Keep fast control loops on the RT thread, move heavy computation to `.compute()`, and use `.cores()` to prevent contention. - **Use `.prefer_rt()` not `.require_rt()`.** During development you rarely have RT permissions. `.prefer_rt()` degrades gracefully; `.require_rt()` fails hard. ## Next Steps - [Deterministic mode](/advanced/deterministic-mode) — reproducible simulations with fixed timesteps - [Safety monitor](/advanced/safety-monitor) — graduated watchdog and health states - [Scheduler configuration](/advanced/scheduler-configuration) — shared memory transport and node orchestration --- ## Tutorial 1: IMU Sensor Node (Python) Path: /tutorials/01-sensor-node-python Description: Read IMU sensor data and publish it over a topic — Python edition # Tutorial 1: IMU Sensor Node (Python) > **Rust version:** [Tutorial 1: IMU Sensor Node](/tutorials/01-sensor-node) Build a node that simulates IMU sensor readings (accelerometer + gyroscope) and publishes them over a topic. A second node subscribes and displays the data. ## What You'll Learn - Creating a Python node with `horus.Node()` - Publishing data with `node.send()` - Subscribing with `node.recv()` - Running multiple nodes with `horus.run()` - Setting execution order and tick rate ## Step 1: Create the Project ```bash horus new imu-tutorial -p cd imu-tutorial ``` ## Step 2: Write the Code Replace `main.py`: ```python import horus import math # ── Sensor Node ────────────────────────────────────────────── def make_sensor(): """Simulates an IMU producing accelerometer and gyroscope readings.""" t = [0.0] def tick(node): t[0] += horus.dt() # Simulate accelerometer (gravity + vibration) accel_x = 0.1 * math.sin(t[0] * 2.0) accel_y = 0.05 * math.cos(t[0] * 3.0) accel_z = 9.81 + 0.02 * math.sin(t[0] * 10.0) # Simulate gyroscope (slow rotation) gyro_roll = 0.01 * math.sin(t[0]) gyro_pitch = 0.02 * math.cos(t[0] * 0.5) gyro_yaw = 0.05 reading = { "accel_x": accel_x, "accel_y": accel_y, "accel_z": accel_z, "gyro_roll": gyro_roll, "gyro_pitch": gyro_pitch, "gyro_yaw": gyro_yaw, "timestamp": horus.now(), } node.send("imu.data", reading) return horus.Node(name="ImuSensor", tick=tick, rate=100, order=0, pubs=["imu.data"]) # ── Display Node ───────────────────────────────────────────── def make_display(): """Prints every 100th IMU sample to avoid flooding the terminal.""" count = [0] def tick(node): msg = node.recv("imu.data") if msg is not None: count[0] += 1 if count[0] % 100 == 0: print(f"[#{count[0]}] accel=({msg['accel_x']:.3f}, {msg['accel_y']:.3f}, {msg['accel_z']:.2f})" f" gyro=({msg['gyro_roll']:.4f}, {msg['gyro_pitch']:.4f}, {msg['gyro_yaw']:.4f})") return horus.Node(name="ImuDisplay", tick=tick, rate=100, order=1, subs=["imu.data"]) # ── Main ───────────────────────────────────────────────────── print("Starting IMU tutorial...\n") horus.run(make_sensor(), make_display()) ``` ## Step 3: Run It ```bash horus run ``` You'll see output every 100 samples: ``` Starting IMU tutorial... [#100] accel=(0.091, -0.042, 9.81) gyro=(0.0084, 0.0193, 0.0500) [#200] accel=(-0.054, 0.048, 9.81) gyro=(0.0059, 0.0100, 0.0500) [#300] accel=(-0.099, -0.013, 9.81) gyro=(-0.0029, -0.0098, 0.0500) ``` Press **Ctrl+C** to stop. ## Understanding the Code ### State Management Python nodes use closures for state. Wrap mutable state in a list (`t = [0.0]`) since closures can't reassign outer variables: ```python t = [0.0] # mutable via t[0] def tick(node): t[0] += horus.dt() # works ``` ### Timing with `horus.dt()` Use `horus.dt()` instead of `time.time()` — it returns the actual timestep and works correctly in deterministic mode: ```python t[0] += horus.dt() # correct — uses framework time ``` ### Execution Order `order=0` runs before `order=1`. The sensor publishes data before the display reads it: ```python sensor = horus.Node(..., order=0) # runs first display = horus.Node(..., order=1) # runs second ``` ## Using Typed Messages For better performance and cross-language compatibility, use typed messages instead of dicts: ```python import horus def tick(node): imu = horus.Imu() # typed message (~50ns vs ~4μs for dicts) # ... set fields ... node.send("imu.data", imu) sensor = horus.Node(name="ImuSensor", tick=tick, rate=100, order=0, pubs=[horus.Imu]) # typed topic ``` ## Experiments 1. **Change the rate** — try `rate=1000` for 1kHz sampling 2. **Add noise** — use `horus.rng_float()` for deterministic random noise 3. **Add a filter node** — subscribe to raw IMU, apply smoothing, publish filtered data ## Next Steps - [Tutorial 2: Motor Controller (Python)](/tutorials/02-motor-controller-python) — subscribe to commands and control a motor - [Tutorial 1 (Rust)](/tutorials/01-sensor-node) — same tutorial in Rust - [Python API Reference](/python/api/python-bindings) — full API docs --- ## Tutorial 2: Motor Controller (Python) Path: /tutorials/02-motor-controller-python Description: Subscribe to velocity commands, simulate motor physics, publish state feedback — Python edition # Tutorial 2: Motor Controller (Python) > **Rust version:** [Tutorial 2: Motor Controller](/tutorials/02-motor-controller) Build a motor controller that subscribes to velocity commands, simulates motor physics, and publishes position/velocity feedback. Includes safe shutdown. ## What You'll Learn - Subscribing to command topics - Publishing state feedback - Managing state between ticks - Multiple topics per node (sub + pub) - Safe shutdown for actuators ## Step 1: Create the Project ```bash horus new motor-tutorial -p cd motor-tutorial ``` ## Step 2: Write the Code Replace `main.py`: ```python import horus import math # ── Commander Node ─────────────────────────────────────────── def make_commander(): """Generates sine-wave velocity commands.""" t = [0.0] def tick(node): t[0] += horus.dt() velocity = 2.0 * math.sin(t[0] * 0.5) # oscillate ±2 rad/s node.send("motor.cmd", {"velocity": velocity, "max_torque": 5.0}) return horus.Node(name="Commander", tick=tick, rate=100, order=0, pubs=["motor.cmd"]) # ── Motor Controller Node ─────────────────────────────────── def make_motor(): """Simulates a motor: integrates velocity → position.""" state = {"position": 0.0, "velocity": 0.0} def tick(node): cmd = node.recv("motor.cmd") if cmd is not None: state["velocity"] = cmd["velocity"] # Integrate velocity → position dt = horus.dt() state["position"] += state["velocity"] * dt # Publish state feedback node.send("motor.state", { "position": state["position"], "velocity": state["velocity"], "timestamp": horus.now(), }) def shutdown(node): # SAFETY: stop the motor before exiting state["velocity"] = 0.0 node.send("motor.cmd", {"velocity": 0.0, "max_torque": 0.0}) print("Motor stopped safely") return horus.Node(name="MotorController", tick=tick, shutdown=shutdown, rate=100, order=1, subs=["motor.cmd"], pubs=["motor.state"]) # ── Display Node ──────────────────────────────────────────── def make_display(): """Prints motor state every 50 samples.""" count = [0] def tick(node): msg = node.recv("motor.state") if msg is not None: count[0] += 1 if count[0] % 50 == 0: print(f"[#{count[0]}] pos={msg['position']:.2f} rad" f" vel={msg['velocity']:.2f} rad/s") return horus.Node(name="StateDisplay", tick=tick, rate=100, order=2, subs=["motor.state"]) # ── Main ──────────────────────────────────────────────────── print("Starting motor controller tutorial...\n") horus.run(make_commander(), make_motor(), make_display()) ``` ## Step 3: Run It ```bash horus run ``` ``` Starting motor controller tutorial... [#50] pos=0.12 rad vel=0.50 rad/s [#100] pos=0.95 rad vel=1.73 rad/s [#150] pos=2.84 rad vel=1.98 rad/s [#200] pos=4.76 rad vel=0.97 rad/s ``` Press **Ctrl+C** — you'll see "Motor stopped safely" confirming the shutdown callback ran. ## Understanding the Code ### Safe Shutdown The `shutdown` callback is critical for actuator nodes. When you press Ctrl+C, HORUS calls `shutdown()` on every node before exiting: ```python def shutdown(node): state["velocity"] = 0.0 node.send("motor.cmd", {"velocity": 0.0, "max_torque": 0.0}) print("Motor stopped safely") ``` Without this, a real motor would continue at its last commanded velocity. ### Data Flow ``` Commander (order=0) → motor.cmd → MotorController (order=1) → motor.state → Display (order=2) ``` Lower order numbers run first each tick, ensuring the commander publishes before the controller reads. ### State Between Ticks The `state` dict persists across ticks via closure. The motor integrates velocity into position every tick: ```python state["position"] += state["velocity"] * dt ``` ## Using a Class for State For complex nodes, a class is cleaner than closures: ```python class Motor: def __init__(self): self.position = 0.0 self.velocity = 0.0 def tick(self, node): cmd = node.recv("motor.cmd") if cmd is not None: self.velocity = cmd["velocity"] self.position += self.velocity * horus.dt() node.send("motor.state", {"position": self.position, "velocity": self.velocity}) def shutdown(self, node): self.velocity = 0.0 print("Motor stopped safely") motor = Motor() node = horus.Node(name="Motor", tick=motor.tick, shutdown=motor.shutdown, rate=100, order=1, subs=["motor.cmd"], pubs=["motor.state"]) ``` ## Next Steps - [Tutorial 3: Full Robot (Python)](/tutorials/03-full-robot-python) — combine sensors and motors into a complete system - [Tutorial 2 (Rust)](/tutorials/02-motor-controller) — same tutorial in Rust - [Common Mistakes](/getting-started/common-mistakes) — avoid shutdown pitfalls --- ## Tutorial 3: Full Robot System (Python) Path: /tutorials/03-full-robot-python Description: Combine sensors, controller, and state estimator into a complete multi-rate robot — Python edition # Tutorial 3: Full Robot System (Python) > **Rust version:** [Tutorial 3: Full Robot](/tutorials/03-full-robot) Combine an IMU sensor, velocity commander, motor controller, and state estimator into a complete robot system with multi-rate scheduling. ## What You'll Learn - Composing 4+ nodes into a working system - Multi-rate scheduling (different nodes at different frequencies) - Data fusion (subscribing to multiple topics) - Monitoring with `horus monitor` and `horus topic echo` ## Architecture ``` ImuSensor (100Hz) ──→ imu.data ──→ StateEstimator (100Hz) ──→ robot.pose Commander (10Hz) ──→ motor.cmd ──→ MotorController (50Hz) ──→ motor.state ──→ StateEstimator ``` ## The Code ```python import horus import math us = horus.us # microsecond constant for budget/deadline # ── IMU Sensor (100 Hz) ───────────────────────────────────── def make_imu(): t = [0.0] def tick(node): t[0] += horus.dt() node.send("imu.data", { "accel_x": 0.1 * math.sin(t[0] * 2.0), "accel_y": 0.05 * math.cos(t[0] * 3.0), "accel_z": 9.81, "gyro_yaw": 0.05, # constant rotation "timestamp": horus.now(), }) return horus.Node(name="ImuSensor", tick=tick, rate=100, order=0, pubs=["imu.data"]) # ── Commander (10 Hz) ──────────────────────────────────────── def make_commander(): t = [0.0] def tick(node): t[0] += horus.dt() node.send("motor.cmd", { "velocity": 1.0 + 0.5 * math.sin(t[0] * 0.3), }) return horus.Node(name="Commander", tick=tick, rate=10, order=1, pubs=["motor.cmd"]) # ── Motor Controller (50 Hz) ──────────────────────────────── def make_motor(): state = {"velocity": 0.0, "position": 0.0} def tick(node): cmd = node.recv("motor.cmd") if cmd is not None: state["velocity"] = cmd["velocity"] state["position"] += state["velocity"] * horus.dt() node.send("motor.state", { "position": state["position"], "velocity": state["velocity"], }) def shutdown(node): state["velocity"] = 0.0 print("Motor stopped safely") return horus.Node(name="MotorController", tick=tick, shutdown=shutdown, rate=50, order=2, subs=["motor.cmd"], pubs=["motor.state"]) # ── State Estimator (100 Hz) ──────────────────────────────── def make_estimator(): """Fuses IMU gyro + motor velocity into a robot pose estimate.""" pose = {"x": 0.0, "y": 0.0, "heading": 0.0} def tick(node): dt = horus.dt() # Fuse IMU data (heading from gyro) imu = node.recv("imu.data") if imu is not None: pose["heading"] += imu["gyro_yaw"] * dt # Fuse motor state (position from velocity) motor = node.recv("motor.state") if motor is not None: pose["x"] += motor["velocity"] * math.cos(pose["heading"]) * dt pose["y"] += motor["velocity"] * math.sin(pose["heading"]) * dt node.send("robot.pose", { "x": pose["x"], "y": pose["y"], "heading": pose["heading"], "timestamp": horus.now(), }) return horus.Node(name="StateEstimator", tick=tick, rate=100, order=3, subs=["imu.data", "motor.state"], pubs=["robot.pose"]) # ── Main ──────────────────────────────────────────────────── print("Starting full robot system...") print(" ImuSensor: 100 Hz (order 0)") print(" Commander: 10 Hz (order 1)") print(" MotorController: 50 Hz (order 2)") print(" StateEstimator: 100 Hz (order 3)") print() horus.run( make_imu(), make_commander(), make_motor(), make_estimator(), tick_rate=100, watchdog_ms=500, ) ``` ## Run and Monitor ```bash # Terminal 1: Run the robot horus run # Terminal 2: Monitor topics horus topic echo robot.pose # Terminal 3: Full monitoring dashboard horus monitor ``` ## Key Concepts ### Multi-Rate Scheduling Each node runs at its own rate. The scheduler handles the timing: ```python make_imu() # rate=100 — ticks 100 times per second make_commander() # rate=10 — ticks 10 times per second make_motor() # rate=50 — ticks 50 times per second make_estimator() # rate=100 — ticks 100 times per second ``` ### Data Fusion The state estimator subscribes to **two** topics and fuses them: ```python imu = node.recv("imu.data") # gyro → heading motor = node.recv("motor.state") # velocity → position ``` Always call `recv()` on every subscribed topic every tick — even if you don't need the data yet. This prevents stale data accumulation. ### Watchdog `watchdog_ms=500` detects frozen nodes. If any node's tick takes longer than 500ms, the safety monitor triggers. ## Using the Scheduler Directly For production configs with RT features: ```python scheduler = horus.Scheduler(tick_rate=100, watchdog_ms=500, rt=True) scheduler.add(make_imu()) scheduler.add(make_commander()) scheduler.add(make_motor()) scheduler.add(make_estimator()) scheduler.run() ``` ## Next Steps - [Tutorial 4: Custom Messages (Python)](/tutorials/04-custom-messages-python) — define your own message types - [Tutorial 3 (Rust)](/tutorials/03-full-robot) — same tutorial in Rust - [Scheduler API](/python/api/python-bindings#scheduler) — advanced scheduling --- ## Tutorial 1: Build an IMU Sensor Node Path: /tutorials/01-sensor-node Description: Create a node that publishes IMU sensor data — accelerometer and gyroscope readings # Tutorial 1: Build an IMU Sensor Node In this tutorial, you'll build a node that simulates an IMU (Inertial Measurement Unit) sensor — the kind found in every drone, robot, and smartphone. **Prerequisites:** [Quick Start](/getting-started/quick-start) completed, HORUS installed. **What you'll learn:** - Custom data structures on topics - Publishing at a specific rate - Using `DurationExt` for ergonomic timing (`.hz()`, `.ms()`) **Time:** 15 minutes --- ## What We're Building An IMU sensor node that: 1. Generates accelerometer data (x, y, z in m/s²) 2. Generates gyroscope data (roll, pitch, yaw rates in rad/s) 3. Publishes both on a topic at 100Hz 4. A display node that prints the data ## Step 1: Create the Project ```bash horus new imu-demo -r cd imu-demo ``` ## Step 2: Define the IMU Data Type First, we need a struct to hold IMU readings. In HORUS, any type that implements `Copy` can be sent over a topic. Replace `src/main.rs` with: ```rust use horus::prelude::*; /// IMU sensor reading — accelerometer + gyroscope. /// /// This struct is Copy, so HORUS can send it over shared memory /// without serialization overhead. #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct ImuData { // Accelerometer (m/s^2) accel_x: f32, accel_y: f32, accel_z: f32, // Gyroscope (rad/s) gyro_roll: f32, gyro_pitch: f32, gyro_yaw: f32, // Timestamp (seconds since start) timestamp: f64, } ``` ## Step 3: Build the Sensor Node ```rust struct ImuSensor { publisher: Topic, tick_count: u64, } impl ImuSensor { fn new() -> Result { Ok(Self { publisher: Topic::new("imu/data")?, tick_count: 0, }) } } impl Node for ImuSensor { fn name(&self) -> &str { "ImuSensor" } fn tick(&mut self) { let t = self.tick_count as f64 * 0.01; // 100Hz → 0.01s per tick // Simulate a robot turning slowly let data = ImuData { accel_x: 0.0, accel_y: 0.0, accel_z: 9.81, // Gravity gyro_roll: 0.0, gyro_pitch: 0.0, gyro_yaw: 0.1 * (t * 0.5).sin(), // Gentle oscillation timestamp: t, }; self.publisher.send(data); self.tick_count += 1; } // SAFETY: shutdown() ensures clean sensor lifecycle. Log final state // so operators can verify the sensor was active until shutdown. fn shutdown(&mut self) -> Result<()> { eprintln!("ImuSensor shutting down after {} ticks", self.tick_count); Ok(()) } } ``` Key points: - `Topic::new("imu/data")` creates a topic that carries `ImuData` structs - `tick()` runs at whatever rate the scheduler sets (we'll set 100Hz below) - The simulated data includes gravity on the Z-axis and a gentle yaw oscillation ## Step 4: Build the Display Node ```rust struct ImuDisplay { subscriber: Topic, sample_count: u64, } impl ImuDisplay { fn new() -> Result { Ok(Self { subscriber: Topic::new("imu/data")?, sample_count: 0, }) } } impl Node for ImuDisplay { fn name(&self) -> &str { "ImuDisplay" } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the topic buffer. // Skipping ticks causes stale data accumulation in shared memory. if let Some(data) = self.subscriber.recv() { self.sample_count += 1; // Print every 100th sample (once per second at 100Hz) if self.sample_count % 100 == 0 { println!( "[{:.1}s] accel=({:.2}, {:.2}, {:.2}) gyro=({:.3}, {:.3}, {:.3})", data.timestamp, data.accel_x, data.accel_y, data.accel_z, data.gyro_roll, data.gyro_pitch, data.gyro_yaw, ); } } } // SAFETY: shutdown() logs final sample count for diagnostics. fn shutdown(&mut self) -> Result<()> { eprintln!("ImuDisplay shutting down after {} samples", self.sample_count); Ok(()) } } ``` ## Step 5: Wire It Together ```rust fn main() -> Result<()> { eprintln!("IMU demo starting...\n"); let mut scheduler = Scheduler::new(); // Execution order: sensor publishes (0) before display subscribes (1). scheduler.add(ImuSensor::new()?) .order(0) // NOTE: .rate(100.hz()) triggers auto-RT detection at finalize(). .rate(100.hz()) // 100Hz — typical IMU rate .build()?; scheduler.add(ImuDisplay::new()?) .order(1) .build()?; scheduler.run() } ``` Notice `.rate(100.hz())` — this tells the scheduler to tick this node at 100Hz. The `.hz()` method comes from HORUS's `DurationExt` trait, which also gives you `.ms()`, `.us()`, and `.khz()`. ## Step 6: Run It ```bash horus run ``` Expected output (one line per second): ``` IMU demo starting... [1.0s] accel=(0.00, 0.00, 9.81) gyro=(0.000, 0.000, 0.048) [2.0s] accel=(0.00, 0.00, 9.81) gyro=(0.000, 0.000, 0.084) [3.0s] accel=(0.00, 0.00, 9.81) gyro=(0.000, 0.000, 0.097) ``` ## What You Learned - **Custom data types** on topics (`ImuData` struct with `#[derive(Copy)]`) - **Per-node rates** with `.rate(100.hz())` - **Sampling** to display data at a readable rate (print every 100th sample) - **Topic naming** with namespaces (`"imu/data"`) ## Next: [Tutorial 2: Build a Motor Controller](/tutorials/02-motor-controller) In the next tutorial, we'll build a motor controller that subscribes to velocity commands and publishes joint positions — the other half of a robot. --- ## Tutorial 4: Custom Messages (Python) Path: /tutorials/04-custom-messages-python Description: Send typed messages, dicts, and dataclasses between Python nodes — Python edition # Tutorial 4: Custom Messages (Python) > **Rust version:** [Tutorial 4: Custom Messages](/tutorials/04-custom-messages) Learn three ways to send data between Python nodes: typed messages (fast), dicts (flexible), and dataclasses (structured). ## What You'll Learn - Using built-in typed messages (`CmdVel`, `Imu`, `Odometry`) - Sending Python dicts as generic messages - Performance trade-offs between approaches - When to use which approach ## Approach 1: Built-in Typed Messages (Fastest) Use HORUS's built-in message types for maximum performance (~500ns latency, zero-copy with Rust nodes): ```python import horus def sensor_tick(node): imu = horus.Imu() # Imu fields are arrays: orientation[4], angular_velocity[3], linear_acceleration[3] node.send("imu", imu) def motor_tick(node): cmd = horus.CmdVel(linear=1.0, angular=0.5) node.send("cmd_vel", cmd) def controller_tick(node): imu = node.recv("imu") if imu is not None: # React to IMU data cmd = horus.CmdVel(linear=0.5, angular=0.0) node.send("cmd_vel", cmd) sensor = horus.Node(name="Sensor", tick=sensor_tick, rate=100, order=0, pubs=[horus.Imu]) # typed topic motor = horus.Node(name="Motor", tick=motor_tick, rate=100, order=1, pubs=[horus.CmdVel]) ctrl = horus.Node(name="Controller", tick=controller_tick, rate=100, order=2, subs=[horus.Imu], pubs=[horus.CmdVel]) horus.run(sensor, ctrl) ``` **Available types:** `CmdVel`, `Imu`, `Odometry`, `LaserScan`, `Pose2D`, `Pose3D`, `Twist`, `BatteryState`, `JointState`, `MotorCommand`, `ServoCommand`, `EmergencyStop`, and 60+ more. ## Approach 2: Python Dicts (Most Flexible) Send any Python dict. Uses MessagePack serialization (~4μs latency): ```python import horus def sensor_tick(node): reading = { "temperature": 23.5, "humidity": 0.65, "pressure": 1013.25, "location": "lab_room_3", "tags": ["indoor", "calibrated"], } node.send("environment", reading) def display_tick(node): data = node.recv("environment") if data is not None: print(f"Temp: {data['temperature']}°C, Humidity: {data['humidity']*100:.0f}%") sensor = horus.Node(name="EnvSensor", tick=sensor_tick, rate=1, order=0, pubs=["environment"]) display = horus.Node(name="Display", tick=display_tick, rate=1, order=1, subs=["environment"]) horus.run(sensor, display, duration=10.0) ``` Dicts can contain: strings, numbers, booleans, lists, nested dicts. They're great for prototyping and configuration data. ## Approach 3: Dataclasses (Structured) For structured Python data, use dataclasses and serialize to dicts: ```python import horus from dataclasses import dataclass, asdict @dataclass class BatteryReading: voltage: float current: float temperature: float charge_percent: float @dataclass class BatteryAlert: level: str # "info", "warning", "critical" message: str voltage: float def battery_tick(node): reading = BatteryReading( voltage=11.8 + horus.rng_float() * 0.4, current=2.5, temperature=35.0 + horus.rng_float() * 5.0, charge_percent=75.0, ) node.send("battery.raw", asdict(reading)) def monitor_tick(node): data = node.recv("battery.raw") if data is not None: reading = BatteryReading(**data) if reading.voltage < 11.5: alert = BatteryAlert("warning", "Low voltage", reading.voltage) node.send("battery.alert", asdict(alert)) node.log_warning(f"Battery low: {reading.voltage:.1f}V") battery = horus.Node(name="Battery", tick=battery_tick, rate=1, order=0, pubs=["battery.raw"]) monitor = horus.Node(name="Monitor", tick=monitor_tick, rate=1, order=1, subs=["battery.raw"], pubs=["battery.alert"]) horus.run(battery, monitor, duration=30.0) ``` ## Performance Comparison | Approach | Latency | Cross-language | Use When | |----------|---------|----------------|----------| | **Typed messages** (`CmdVel`, `Imu`) | ~500ns | Rust + Python | Control loops, sensor data, production | | **Python dicts** | ~4μs | Python only | Prototyping, config, low-frequency data | | **Dataclasses → dict** | ~4μs | Python only | Structured data, validation needed | **Rule of thumb:** Use typed messages for anything above 10Hz. Use dicts for everything else. ## Next Steps - [Tutorial 5: Hardware Drivers (Python)](/tutorials/05-hardware-drivers-python) — connect to real hardware - [Tutorial 4 (Rust)](/tutorials/04-custom-messages) — same tutorial in Rust with `message!` macro - [Message Types](/concepts/message-types) — full list of 60+ built-in types --- ## Tutorial 2: Build a Motor Controller Path: /tutorials/02-motor-controller Description: Create a motor controller node that subscribes to velocity commands and publishes joint position # Tutorial 2: Build a Motor Controller In this tutorial, you'll build a motor controller — a node that receives velocity commands and tracks joint position. This is the actuator side of a robot, complementing the sensor from Tutorial 1. **Prerequisites:** [Tutorial 1: IMU Sensor Node](/tutorials/01-sensor-node) completed. **What you'll learn:** - Subscribing to command topics - Publishing state feedback - Managing state between ticks (integration) - Multiple topics per node **Time:** 15 minutes --- ## What We're Building A motor controller node that: 1. Subscribes to velocity commands on `"motor/command"` 2. Integrates velocity into position (simple physics) 3. Publishes current position on `"motor/position"` 4. A commander node that sends test commands ## Step 1: Create the Project ```bash horus new motor-demo -r cd motor-demo ``` ## Step 2: Define the Data Types ```rust use horus::prelude::*; /// Velocity command sent to the motor. #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct MotorCommand { velocity: f32, // Desired velocity (rad/s) max_torque: f32, // Torque limit (N*m) } /// Motor state feedback. #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct MotorState { position: f32, // Current position (radians) velocity: f32, // Current velocity (rad/s) torque: f32, // Applied torque (N*m) timestamp: f64, } ``` ## Step 3: Build the Motor Controller This is the core of the tutorial — a node that subscribes AND publishes: ```rust struct MotorController { commands: Topic, // Subscribe to commands state_pub: Topic, // Publish state position: f32, // Accumulated position velocity: f32, // Current velocity dt: f32, // Time step (set from rate) } impl MotorController { fn new(rate_hz: f32) -> Result { Ok(Self { commands: Topic::new("motor/command")?, state_pub: Topic::new("motor/state")?, position: 0.0, velocity: 0.0, dt: 1.0 / rate_hz, }) } } impl Node for MotorController { fn name(&self) -> &str { "MotorController" } fn tick(&mut self) { // IMPORTANT: call recv() every tick to consume the latest command. // Even if no command arrives, the motor continues integrating position. if let Some(cmd) = self.commands.recv() { // Simple velocity tracking with torque limit let error = cmd.velocity - self.velocity; let torque = error.clamp(-cmd.max_torque, cmd.max_torque); self.velocity += torque * self.dt; } // Integrate velocity → position self.position += self.velocity * self.dt; // Publish current state self.state_pub.send(MotorState { position: self.position, velocity: self.velocity, torque: 0.0, timestamp: 0.0, // Simplified for tutorial }); } // SAFETY: shutdown() is CRITICAL for actuator nodes. Without it, the motor // could continue running at its last commanded velocity after the scheduler stops. // Always zero velocity and publish the stopped state before returning. fn shutdown(&mut self) -> Result<()> { // CRITICAL: zero velocity before publishing — order matters. self.velocity = 0.0; self.state_pub.send(MotorState { position: self.position, velocity: 0.0, torque: 0.0, timestamp: 0.0, }); eprintln!("Motor stopped safely at position {:.2} rad", self.position); Ok(()) } } ``` Key points: - **Two topics**: one for receiving commands, one for publishing state - **State between ticks**: `position` and `velocity` persist across `tick()` calls - **Simple physics**: velocity integrates into position (`position += velocity * dt`) - **Safe shutdown**: motor velocity set to zero in `shutdown()` ## Step 4: Build the Commander A test node that sends velocity commands: ```rust struct Commander { publisher: Topic, tick_count: u64, } impl Commander { fn new() -> Result { Ok(Self { publisher: Topic::new("motor/command")?, tick_count: 0, }) } } impl Node for Commander { fn name(&self) -> &str { "Commander" } fn tick(&mut self) { let t = self.tick_count as f64 * 0.01; // At 100Hz // Send a sine wave velocity command let velocity = 1.0 * (t * 0.5).sin() as f32; self.publisher.send(MotorCommand { velocity, max_torque: 10.0, }); self.tick_count += 1; } // SAFETY: shutdown() sends a zero-velocity command so the motor stops // even if the motor controller's own shutdown() hasn't run yet. fn shutdown(&mut self) -> Result<()> { self.publisher.send(MotorCommand { velocity: 0.0, max_torque: 0.0 }); eprintln!("Commander shutting down, sent zero-velocity command."); Ok(()) } } ``` ## Step 5: Add a State Display ```rust struct StateDisplay { subscriber: Topic, sample_count: u64, } impl StateDisplay { fn new() -> Result { Ok(Self { subscriber: Topic::new("motor/state")?, sample_count: 0, }) } } impl Node for StateDisplay { fn name(&self) -> &str { "StateDisplay" } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the topic buffer. if let Some(state) = self.subscriber.recv() { self.sample_count += 1; if self.sample_count % 50 == 0 { println!( "pos={:.2} rad vel={:.2} rad/s", state.position, state.velocity, ); } } } // SAFETY: shutdown() logs final display state for diagnostics. fn shutdown(&mut self) -> Result<()> { eprintln!("StateDisplay shutting down after {} samples", self.sample_count); Ok(()) } } ``` ## Step 6: Wire Everything Together ```rust fn main() -> Result<()> { eprintln!("Motor controller demo starting...\n"); let rate = 100.0; // Hz let mut scheduler = Scheduler::new(); // Execution order: Commander (0) → MotorController (1) → StateDisplay (2). // Commander must publish before motor reads; motor must publish before display reads. scheduler.add(Commander::new()?) .order(0) // NOTE: .rate(100.hz()) triggers auto-RT detection at finalize(). .rate(100.hz()) .build()?; scheduler.add(MotorController::new(rate)?) .order(1) // NOTE: .rate(100.hz()) triggers auto-RT detection at finalize(). .rate(100.hz()) .build()?; scheduler.add(StateDisplay::new()?) .order(2) .build()?; scheduler.run() } ``` Notice the data flow: **Commander (0) → MotorController (1) → StateDisplay (2)**. Order matters — the commander must send before the controller reads. ## Step 7: Run It ```bash horus run ``` Expected output: ``` Motor controller demo starting... pos=0.02 rad vel=0.05 rad/s pos=0.26 rad vel=0.44 rad/s pos=0.86 rad vel=0.76 rad/s pos=1.58 rad vel=0.84 rad/s ``` Press Ctrl+C to stop — you'll see the shutdown message: ``` Motor stopped safely at position 3.14 rad ``` ## What You Learned - **Subscribing** to commands with `commands.recv()` - **Publishing** state feedback with `state_pub.send()` - **State management** between ticks (velocity integration) - **Safe shutdown** — always stop motors in `shutdown()` - **Data flow ordering** — commander before controller before display ## Next: [Tutorial 3: Full Robot Integration](/tutorials/03-full-robot) In the next tutorial, we'll combine the IMU sensor and motor controller into a complete robot system with coordinate frame tracking and monitoring. --- ## Tutorial 5: Hardware & Real-Time (Python) Path: /tutorials/05-hardware-and-rt-python Description: Load hardware drivers and configure real-time scheduling — Python edition # Tutorial 5: Hardware & Real-Time (Python) > **Rust versions:** [Tutorial 5: Hardware Drivers](/tutorials/05-hardware-drivers) and [Real-Time Control](/tutorials/realtime-control) Learn to load hardware drivers from `horus.toml` and configure real-time scheduling with budgets, deadlines, and miss policies. ## What You'll Learn - Loading hardware drivers with `horus.drivers.load()` - Accessing driver configuration parameters - Setting budgets and deadlines for timing guarantees - Handling deadline misses with `on_miss` - Using `compute=True` for CPU-heavy nodes - Production scheduler configuration ## Part 1: Hardware Drivers ### Configure Drivers in horus.toml ```toml # horus.toml [package] name = "my-robot" version = "0.1.0" [drivers] arm = { type = "dynamixel", port = "/dev/ttyUSB0", baudrate = 1000000, servo_ids = [1, 2, 3] } lidar = { type = "rplidar", port = "/dev/ttyUSB1", scan_mode = "standard" } imu = { type = "i2c", bus = 1, address = "0x68" } ``` ### Load and Use Drivers ```python import horus # Load drivers from horus.toml hw = horus.drivers.load() # Check what's available print(f"Drivers: {hw.list()}") # ["arm", "lidar", "imu"] if hw.has("arm"): arm = hw.dynamixel("arm") port = arm.get_or("port", "/dev/ttyUSB0") servo_ids = arm.get_or("servo_ids", [1, 2, 3]) print(f"Arm on {port}, servos: {servo_ids}") if hw.has("lidar"): lidar = hw.rplidar("lidar") scan_mode = lidar.get_or("scan_mode", "standard") print(f"LiDAR in {scan_mode} mode") def arm_tick(node): cmd = node.recv("arm.command") if cmd is not None: # Send to hardware via driver params node.send("arm.state", {"position": [0.0, 0.0, 0.0]}) arm_node = horus.Node(name="ArmController", tick=arm_tick, rate=100, order=0, subs=["arm.command"], pubs=["arm.state"]) horus.run(arm_node) ``` ## Part 2: Real-Time Scheduling ### Budget and Deadline Set timing constraints to detect overruns: ```python import horus us = horus.us # 1e-6 (microseconds → seconds) ms = horus.ms # 1e-3 (milliseconds → seconds) # Motor controller: 1kHz with 800μs budget, 950μs deadline motor = horus.Node( name="MotorCtrl", tick=motor_tick, rate=1000, order=0, budget=800 * us, # must finish tick within 800μs deadline=950 * us, # hard deadline at 950μs on_miss="safe_mode", # enter safe state if deadline missed core=0, # pin to CPU core 0 ) # LiDAR driver: 100Hz, skip missed ticks lidar = horus.Node( name="LidarDriver", tick=lidar_tick, rate=100, order=1, on_miss="skip", # skip tick on deadline miss ) # Path planner: CPU-heavy, runs on thread pool planner = horus.Node( name="PathPlanner", tick=planner_tick, rate=10, order=5, compute=True, # runs on worker thread pool, not main loop on_miss="warn", # just log a warning ) # Production scheduler with watchdog and RT horus.run( motor, lidar, planner, tick_rate=1000, rt=True, # request SCHED_FIFO (graceful fallback) watchdog_ms=500, # detect frozen nodes blackbox_mb=64, # flight recorder for debugging ) ``` ### Deadline Miss Policies | Policy | Behavior | Use For | |--------|----------|---------| | `"warn"` | Log warning, continue normally | Non-critical nodes (logging, telemetry) | | `"skip"` | Skip this tick, resume next cycle | Sensor drivers that can miss a frame | | `"safe_mode"` | Call `enter_safe_state()` equivalent, continue ticking | Motor controllers, actuators | | `"stop"` | Stop the entire scheduler | Safety-critical nodes | ### Adaptive Quality with Budget Use `horus.budget_remaining()` to do more work when time permits: ```python def planner_tick(node): # Always compute basic path path = compute_basic_path() # Only optimize if budget allows if horus.budget_remaining() > 2 * ms: path = optimize_path(path) # Only smooth if still have time if horus.budget_remaining() > 1 * ms: path = smooth_path(path) node.send("path", path) ``` ### Complete Production Example ```python import horus us, ms = horus.us, horus.ms def safety_tick(node): """Safety monitor — runs first every tick.""" if node.has_msg("emergency_stop"): estop = node.recv("emergency_stop") if estop.get("engaged"): node.log_error("EMERGENCY STOP ENGAGED") node.request_stop() def motor_tick(node): cmd = node.recv("cmd_vel") if cmd is not None: # Apply velocity command to motor node.send("motor.state", {"velocity": cmd.get("linear", 0.0)}) def motor_shutdown(node): node.send("cmd_vel", {"linear": 0.0, "angular": 0.0}) node.log_info("Motors stopped safely") safety = horus.Node(name="Safety", tick=safety_tick, rate=100, order=0, subs=["emergency_stop"]) motor = horus.Node(name="Motor", tick=motor_tick, shutdown=motor_shutdown, rate=100, order=1, budget=500 * us, on_miss="safe_mode", subs=["cmd_vel"], pubs=["motor.state"]) horus.run(safety, motor, tick_rate=100, rt=True, watchdog_ms=500) ``` ## Next Steps - [Real-Time Concepts](/concepts/real-time) — what real-time means for robotics - [Safety Monitor](/advanced/safety-monitor) — graduated degradation and watchdog - [Choosing a Language](/getting-started/choosing-language) — when to use Python vs Rust for RT - [Python API Reference](/python/api/python-bindings) — full scheduling kwargs --- ## Tutorial 3: Full Robot Integration Path: /tutorials/03-full-robot Description: Combine sensor and motor nodes into a complete robot system with TransformFrame and monitoring # Tutorial 3: Full Robot Integration In this tutorial, you'll combine the IMU sensor from Tutorial 1 and the motor controller from Tutorial 2 into a complete robot system. You'll add coordinate frame tracking with TransformFrame and use `horus monitor` to visualize the system. **Prerequisites:** [Tutorial 1](/tutorials/01-sensor-node) and [Tutorial 2](/tutorials/02-motor-controller) completed. **What you'll learn:** - Composing multiple node types into one system - TransformFrame for coordinate frame tracking - Multi-rate scheduling (sensors at 100Hz, control at 50Hz, display at 1Hz) - Using `horus monitor` for system visualization **Time:** 20 minutes --- ## What We're Building A robot system with 4 nodes: ``` ImuSensor (100Hz) ──→ "imu/data" ──→ StateEstimator (100Hz) │ Commander (10Hz) ──→ "motor/cmd" ──→ MotorController (50Hz) │ "motor/state" ──→ StateEstimator │ TransformFrame ("base_link" → "world") ``` ## Step 1: Create the Project ```bash horus new robot-integration -r cd robot-integration ``` ## Step 2: Define Shared Types We'll reuse the types from Tutorials 1 and 2, plus add a robot pose: ```rust use horus::prelude::*; // ── Data Types ────────────────────────────────────────────── #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct ImuData { accel_x: f32, accel_y: f32, accel_z: f32, gyro_roll: f32, gyro_pitch: f32, gyro_yaw: f32, timestamp: f64, } #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct MotorCommand { velocity: f32, max_torque: f32, } #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct MotorState { position: f32, velocity: f32, torque: f32, timestamp: f64, } #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct RobotPose { x: f32, y: f32, heading: f32, // radians timestamp: f64, } ``` ## Step 3: The IMU Sensor (from Tutorial 1) ```rust struct ImuSensor { publisher: Topic, tick_count: u64, } impl ImuSensor { fn new() -> Result { Ok(Self { publisher: Topic::new("imu/data")?, tick_count: 0 }) } } impl Node for ImuSensor { fn name(&self) -> &str { "ImuSensor" } fn tick(&mut self) { let t = self.tick_count as f64 * 0.01; self.publisher.send(ImuData { accel_x: 0.0, accel_y: 0.0, accel_z: 9.81, gyro_roll: 0.0, gyro_pitch: 0.0, gyro_yaw: 0.1 * (t * 0.5).sin() as f32, timestamp: t, }); self.tick_count += 1; } // SAFETY: shutdown() ensures clean sensor lifecycle and logs tick count // so operators can verify the sensor ran for the expected duration. fn shutdown(&mut self) -> Result<()> { eprintln!("ImuSensor shutting down after {} ticks", self.tick_count); Ok(()) } } ``` ## Step 4: The Motor Controller (from Tutorial 2) ```rust struct MotorController { commands: Topic, state_pub: Topic, position: f32, velocity: f32, } impl MotorController { fn new() -> Result { Ok(Self { commands: Topic::new("motor/cmd")?, state_pub: Topic::new("motor/state")?, position: 0.0, velocity: 0.0, }) } } impl Node for MotorController { fn name(&self) -> &str { "MotorController" } fn tick(&mut self) { let dt = 0.02; // 50Hz // IMPORTANT: call recv() every tick to consume the latest command. if let Some(cmd) = self.commands.recv() { let error = cmd.velocity - self.velocity; self.velocity += error.clamp(-cmd.max_torque, cmd.max_torque) * dt; } self.position += self.velocity * dt; self.state_pub.send(MotorState { position: self.position, velocity: self.velocity, torque: 0.0, timestamp: 0.0, }); } // SAFETY: shutdown() is CRITICAL for actuator nodes. Without it, the motor // could continue at its last commanded velocity after the scheduler stops. // Always zero velocity and publish the stopped state. fn shutdown(&mut self) -> Result<()> { // CRITICAL: zero velocity before returning. self.velocity = 0.0; self.state_pub.send(MotorState { position: self.position, velocity: 0.0, torque: 0.0, timestamp: 0.0, }); eprintln!("Motor stopped at position {:.2} rad", self.position); Ok(()) } } ``` ## Step 5: The State Estimator (NEW) This node fuses IMU and motor data to estimate the robot's pose: ```rust struct StateEstimator { imu_sub: Topic, motor_sub: Topic, pose_pub: Topic, pose: RobotPose, } impl StateEstimator { fn new() -> Result { Ok(Self { imu_sub: Topic::new("imu/data")?, motor_sub: Topic::new("motor/state")?, pose_pub: Topic::new("robot/pose")?, pose: RobotPose::default(), }) } } impl Node for StateEstimator { fn name(&self) -> &str { "StateEstimator" } fn tick(&mut self) { let dt = 0.01; // 100Hz // IMPORTANT: call recv() on each subscription every tick. // Read IMU for heading changes if let Some(imu) = self.imu_sub.recv() { self.pose.heading += imu.gyro_yaw * dt as f32; self.pose.timestamp = imu.timestamp; } // IMPORTANT: call recv() on each subscription every tick. // Read motor state for forward motion if let Some(motor) = self.motor_sub.recv() { // Simple differential drive: motor velocity → forward motion let speed = motor.velocity * 0.1; // Scale to m/s self.pose.x += speed * self.pose.heading.cos() * dt as f32; self.pose.y += speed * self.pose.heading.sin() * dt as f32; } self.pose_pub.send(self.pose); } // SAFETY: shutdown() logs the final estimated pose for post-run analysis. fn shutdown(&mut self) -> Result<()> { eprintln!( "StateEstimator shutting down. Final pose: ({:.2}, {:.2}) heading={:.2} rad", self.pose.x, self.pose.y, self.pose.heading ); Ok(()) } } ``` This is a simplified state estimator — in production you'd use an Extended Kalman Filter (EKF). The point is showing how nodes compose: the estimator subscribes to TWO topics and publishes ONE. ## Step 6: The Commander ```rust struct Commander { publisher: Topic, tick_count: u64, } impl Commander { fn new() -> Result { Ok(Self { publisher: Topic::new("motor/cmd")?, tick_count: 0 }) } } impl Node for Commander { fn name(&self) -> &str { "Commander" } fn tick(&mut self) { let t = self.tick_count as f64 * 0.1; // 10Hz self.publisher.send(MotorCommand { velocity: 1.0 * (t * 0.3).sin() as f32, max_torque: 10.0, }); self.tick_count += 1; } // SAFETY: shutdown() sends a zero-velocity command so the motor stops // even if the motor controller's own shutdown() hasn't run yet. fn shutdown(&mut self) -> Result<()> { self.publisher.send(MotorCommand { velocity: 0.0, max_torque: 0.0 }); eprintln!("Commander shutting down, sent zero-velocity command."); Ok(()) } } ``` ## Step 7: Wire the Complete System Here's where it all comes together — **multi-rate scheduling**: ```rust fn main() -> Result<()> { eprintln!("Robot integration demo starting...\n"); eprintln!("Open another terminal and run: horus monitor\n"); let mut scheduler = Scheduler::new(); // Execution order: ImuSensor (0) → Commander (1) → MotorController (2) → StateEstimator (3). // Sensors and commanders publish before the controller and estimator consume. // Sensor: fastest rate (100Hz) — reads first scheduler.add(ImuSensor::new()?) .order(0) // NOTE: .rate(100.hz()) triggers auto-RT detection at finalize(). .rate(100.hz()) .build()?; // Commander: slow rate (10Hz) — sends high-level commands scheduler.add(Commander::new()?) .order(1) // NOTE: .rate(10.hz()) triggers auto-RT detection at finalize(). .rate(10.hz()) .build()?; // Motor: medium rate (50Hz) — processes commands scheduler.add(MotorController::new()?) .order(2) // NOTE: .rate(50.hz()) triggers auto-RT detection at finalize(). .rate(50.hz()) .build()?; // Estimator: fast rate (100Hz) — fuses all data scheduler.add(StateEstimator::new()?) .order(3) // NOTE: .rate(100.hz()) triggers auto-RT detection at finalize(). .rate(100.hz()) .build()?; scheduler.run() } ``` Notice how different nodes run at different rates: - **IMU at 100Hz** — sensors need high frequency for accuracy - **Commander at 10Hz** — high-level commands change slowly - **Motor at 50Hz** — actuators need moderate update rate - **Estimator at 100Hz** — matches the fastest sensor ## Step 8: Run and Monitor ```bash # Terminal 1: Run the robot horus run # Terminal 2: Open the monitor dashboard horus monitor ``` The monitor shows: - All active nodes and their tick rates - All topics with message counts and rates - System health (CPU, memory, deadline misses) Expected output from the robot: ``` Robot integration demo starting... Open another terminal and run: horus monitor ``` The system runs silently (the estimator publishes pose but nothing prints it). Use `horus topic echo robot/pose` in another terminal to see the pose data: ```bash # Terminal 3: See the robot's estimated pose horus topic echo robot/pose ``` ## What You Learned - **Multi-node composition** — 4 nodes working together - **Multi-rate scheduling** — different rates for different responsibilities - **Data fusion** — state estimator subscribes to multiple topics - **System monitoring** — `horus monitor` and `horus topic echo` - **Execution order** — sensors before controllers before estimators ## What's Next You've built a complete robot system! Here's where to go deeper: - **[TransformFrame](/concepts/transform-frame)** — track coordinate frames between sensors and actuators - **[Real-Time Control](/tutorials/realtime-control)** — add `.budget()`, `.deadline()`, and `.on_miss()` for hard real-time - **[Deterministic Mode](/advanced/deterministic-mode)** — make your system fully reproducible - **[Deployment](/advanced/deployment)** — deploy to a real robot over SSH - **[Safety Monitor](/advanced/safety-monitor)** — add watchdog and graceful degradation ## Complete Code The full `main.rs` for this tutorial is all the code above combined into one file. Each struct and impl block goes in order: types → ImuSensor → MotorController → StateEstimator → Commander → main(). --- ## Tutorial 4: Custom Message Types Path: /tutorials/04-custom-messages Description: Define custom messages for your robot — the message! macro, manual derives, and GenericMessage for dynamic data # Tutorial 4: Custom Message Types HORUS ships with 70+ standard message types, but every real project needs custom messages for its own hardware, protocols, or data formats. This tutorial covers three approaches. **Prerequisites:** [Tutorial 1](/tutorials/01-sensor-node) completed. **What you'll learn:** - Define POD messages with the `message!` macro (zero-copy IPC) - Define complex messages with manual derives (heap types like `String`, `Vec`) - Use `GenericMessage` for dynamic, cross-language data - Publish and subscribe to custom messages in a multi-node system **Time:** 20 minutes --- ## Approach 1: The `message!` Macro (Recommended) For fixed-size structs with only primitive fields (`f32`, `f64`, `u32`, `i64`, `bool`, `[f32; 3]`, etc.), use the `message!` macro. It generates all the boilerplate automatically: ```rust use horus::prelude::*; message! { /// Motor feedback from an actuator MotorFeedback { motor_id: u32, rpm: f32, current_amps: f32, temperature_c: f32, } } ``` **What gets generated:** ```rust #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MotorFeedback { pub motor_id: u32, pub rpm: f32, pub current_amps: f32, pub temperature_c: f32, } impl LogSummary for MotorFeedback { /* Debug-based formatting */ } ``` The resulting type is immediately usable with `Topic<MotorFeedback>` — no additional trait implementations needed. ### Using it ```rust use horus::prelude::*; message! { MotorFeedback { motor_id: u32, rpm: f32, current_amps: f32, temperature_c: f32, } } // Publish let pub_topic: Topic = Topic::new("motor.feedback")?; pub_topic.send(MotorFeedback { motor_id: 1, rpm: 3200.0, current_amps: 1.2, temperature_c: 45.0, }); // Subscribe let sub_topic: Topic = Topic::new("motor.feedback")?; if let Some(msg) = sub_topic.recv() { println!("Motor {} at {} RPM", msg.motor_id, msg.rpm); } ``` ### Multiple messages in one block You can define several messages in a single `message!` call: ```rust use horus::prelude::*; message! { /// Wheel encoder ticks EncoderReading { left_ticks: i64, right_ticks: i64, timestamp_ns: u64, } /// PID controller output PidOutput { setpoint: f64, measured: f64, output: f64, error: f64, } } ``` ### What types can you use? The `message!` macro works with any **fixed-size, Copy** type: | Allowed | Not Allowed | |---------|-------------| | `f32`, `f64` | `String` | | `u8`, `u16`, `u32`, `u64` | `Vec<T>` | | `i8`, `i16`, `i32`, `i64` | `HashMap<K, V>` | | `bool` | `Option<T>` (heap types) | | `[f32; 3]`, `[u8; 256]` | `Box<T>` | For heap-allocated types, use Approach 2. --- ## Approach 2: Manual Derives (Complex Types) When your message needs `String`, `Vec`, `Option`, or nested structs, derive the traits manually: ```rust use horus::prelude::*; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RobotConfig { pub name: String, pub joint_names: Vec, pub max_speeds: Vec, pub description: Option, } ``` This type works with `Topic<RobotConfig>` because it implements `Clone + Serialize + Deserialize`. It uses serialization-based transport instead of zero-copy, which adds ~100ns of overhead — still fast, but not as fast as POD types from `message!`. ### Adding LogSummary If you want debug logging on the topic (via `Topic::new("name")?`), implement `LogSummary`: ```rust use horus::prelude::*; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RobotConfig { pub name: String, pub joint_names: Vec, pub max_speeds: Vec, pub description: Option, } impl LogSummary for RobotConfig { fn log_summary(&self) -> String { format!("RobotConfig({}, {} joints)", self.name, self.joint_names.len()) } } ``` Or derive it for `Debug`-based formatting: ```rust #[derive(Clone, Debug, Serialize, Deserialize, LogSummary)] pub struct RobotConfig { pub name: String, pub joint_names: Vec, pub max_speeds: Vec, pub description: Option, } ``` ### Nested types You can nest custom types — just make sure all nested types also derive the required traits: ```rust use horus::prelude::*; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct WaypointList { pub waypoints: Vec, pub loop_back: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Waypoint { pub x: f64, pub y: f64, pub speed: f64, pub label: String, } ``` --- ## Approach 3: GenericMessage (Dynamic Data) `GenericMessage` is a fixed-size buffer (4KB max) that carries MessagePack-serialized data. Use it when you don't know the schema at compile time, or for quick Rust-Python prototyping. ### Sending structured data ```rust use horus::prelude::*; use std::collections::HashMap; let topic: Topic = Topic::new("experiment_data")?; // from_value() serializes any Serde type into the buffer let mut data = HashMap::new(); data.insert("trial", 42.0); data.insert("accuracy", 0.95); let msg = GenericMessage::from_value(&data)?; topic.send(msg); ``` ### Receiving and deserializing ```rust use horus::prelude::*; use std::collections::HashMap; let topic: Topic = Topic::new("experiment_data")?; if let Some(msg) = topic.recv() { let data: HashMap = msg.to_value()?; println!("Trial {}: accuracy {:.1}%", data["trial"], data["accuracy"] * 100.0); } ``` ### Adding metadata You can attach a string label (up to 255 bytes) to identify the message type at runtime: ```rust use horus::prelude::*; let payload = GenericMessage::from_value(&sensor_data)?; // Or with metadata tag: let raw = rmp_serde::to_vec(&sensor_data)?; let payload = GenericMessage::with_metadata(raw, "lidar_v2".to_string())?; if let Some(msg) = topic.recv() { if let Some(tag) = msg.metadata() { println!("Got message type: {}", tag); } } ``` ### Performance notes | Message Type | IPC Latency | Max Size | |-------------|------------|----------| | `message!` (POD) | ~50ns (zero-copy) | Unlimited | | Manual derive | ~167ns (serde) | Unlimited | | `GenericMessage` | ~4.0-4.4μs | 4KB | Use `GenericMessage` for prototyping. Switch to typed messages for production. --- ## Complete Example: Battery Monitor System Let's build a 2-node system: a battery sensor publishes custom readings, and a monitor checks for low battery and publishes alerts. ```rust use horus::prelude::*; // --- Custom Messages --- message! { /// Raw battery sensor data BatteryReading { cell_count: u32, voltage: f32, current_amps: f32, temperature_c: f32, charge_percent: f32, } } message! { /// Alert when battery is low BatteryAlert { severity: u8, // 1=info, 2=warning, 3=critical charge_percent: f32, voltage: f32, } } // --- Battery Sensor Node --- struct BatterySensor { publisher: Topic, tick_count: u32, } impl BatterySensor { fn new() -> Result { Ok(Self { publisher: Topic::new("battery.raw")?, tick_count: 0, }) } } impl Node for BatterySensor { fn name(&self) -> &str { "BatterySensor" } fn tick(&mut self) { self.tick_count += 1; // Simulate draining battery let charge = 100.0 - (self.tick_count as f32 * 2.5); let voltage = 12.6 - (self.tick_count as f32 * 0.3); let reading = BatteryReading { cell_count: 3, voltage, current_amps: 2.1, temperature_c: 35.0 + (self.tick_count as f32 * 0.5), charge_percent: charge.max(0.0), }; println!("[Battery] {:.0}% ({:.1}V)", reading.charge_percent, reading.voltage); self.publisher.send(reading); } } // --- Battery Monitor Node --- struct BatteryMonitor { subscriber: Topic, alert_pub: Topic, } impl BatteryMonitor { fn new() -> Result { Ok(Self { subscriber: Topic::new("battery.raw")?, alert_pub: Topic::new("battery.alert")?, }) } } impl Node for BatteryMonitor { fn name(&self) -> &str { "BatteryMonitor" } fn tick(&mut self) { if let Some(reading) = self.subscriber.recv() { let severity = if reading.charge_percent < 10.0 { println!("[Monitor] CRITICAL: Battery at {:.0}%!", reading.charge_percent); 3 } else if reading.charge_percent < 30.0 { println!("[Monitor] WARNING: Battery at {:.0}%", reading.charge_percent); 2 } else { return; // No alert needed }; self.alert_pub.send(BatteryAlert { severity, charge_percent: reading.charge_percent, voltage: reading.voltage, }); } } } // --- Main --- fn main() -> Result<()> { println!("=== Battery Monitor System ===\n"); let mut scheduler = Scheduler::new().tick_rate(1_u64.hz()); scheduler.add(BatterySensor::new()?).order(0).build()?; scheduler.add(BatteryMonitor::new()?).order(1).build()?; scheduler.run_for(10_u64.secs())?; println!("\nDone!"); Ok(()) } ``` **Expected output:** ```text === Battery Monitor System === [Battery] 97% (12.3V) [Battery] 95% (12.0V) ... [Battery] 25% (5.1V) [Monitor] WARNING: Battery at 25% [Battery] 22% (4.8V) [Monitor] WARNING: Battery at 22% ... [Battery] 5% (3.0V) [Monitor] CRITICAL: Battery at 5%! ``` --- ## When to Use What | Approach | Use When | Performance | Heap Types | |----------|----------|-------------|------------| | `message!` macro | Fixed-size structs (most cases) | ~50ns (zero-copy) | No | | Manual derive | Need `String`, `Vec`, `Option`, nested structs | ~167ns (serde) | Yes | | `GenericMessage` | Dynamic schemas, quick prototyping, cross-language | ~4μs | N/A (bytes) | **Rules of thumb:** - Start with `message!` — it covers most robotics use cases - Use manual derives when you need heap-allocated fields - Use `GenericMessage` only for prototyping or when the schema isn't known at compile time --- ## Next Steps - [Message Types Reference](/concepts/message-types) — all 70+ built-in messages - [POD Topics](/concepts/core-concepts-podtopic) — how zero-copy IPC works - [Python Custom Messages](/python/api/custom-messages) — using custom messages from Python --- ## Tutorial 5: Hardware Drivers Path: /tutorials/05-hardware-drivers Description: Connect your robot's hardware — configure drivers in horus.toml, load them, and use them in nodes # Tutorial 5: Hardware Drivers Every robot needs to talk to hardware — servos, LiDARs, cameras, IMUs. HORUS separates *what hardware you have* (config) from *how you use it* (code). This tutorial walks through the complete workflow. **Prerequisites:** [Tutorial 1](/tutorials/01-sensor-node) completed. **What you'll learn:** - Configure drivers in `horus.toml` - Load hardware with `drivers::load()` - Use `DriverHandle` and `DriverParams` in nodes - Register custom drivers with `register_driver!` - Test without hardware using `drivers::load_from()` **Time:** 20 minutes --- ## Step 1: Configure Drivers in horus.toml Add a `[drivers]` section to your `horus.toml`. Each driver has a name and a source (`terra`, `package`, or `node`): ```toml [package] name = "my-robot" version = "0.1.0" language = "rust" [drivers.arm] terra = "dynamixel" port = "/dev/ttyUSB0" baudrate = 1000000 servo_ids = [1, 2, 3, 4, 5] [drivers.lidar] terra = "rplidar" port = "/dev/ttyUSB1" baudrate = 256000 [drivers.imu] terra = "i2c" bus = 1 address = 104 [drivers.force_sensor] package = "horus-driver-ati-netft" address = "192.168.1.100" [drivers.conveyor] node = "ConveyorDriver" port = "/dev/ttyACM0" speed = 0.5 ``` Three driver sources: | Key | Source | Example | |-----|--------|---------| | `terra = "..."` | Terra HAL driver (Dynamixel, RPLidar, I2C, etc.) | `terra = "dynamixel"` | | `package = "..."` | Registry package (installed via `horus add`) | `package = "horus-driver-ati-netft"` | | `node = "..."` | Local code (registered with `register_driver!`) | `node = "ConveyorDriver"` | All other keys in the table become typed parameters accessible via `DriverParams`. --- ## Step 2: Load Drivers In your code, call `drivers::load()` to parse the `[drivers]` section and get a `HardwareSet`: ```rust use horus::prelude::*; use horus::drivers; fn main() -> Result<()> { // Loads from horus.toml (searches current dir and parents) let mut hw = drivers::load()?; // See what's configured println!("Drivers: {:?}", hw.list()); // → ["arm", "conveyor", "force_sensor", "imu", "lidar"] Ok(()) } ``` --- ## Step 3: Get Driver Handles Use typed accessors to get handles for Terra drivers: ```rust use horus::prelude::*; use horus::drivers; fn main() -> Result<()> { let mut hw = drivers::load()?; // Typed Terra accessors — validates the driver source matches let arm = hw.dynamixel("arm")?; let lidar = hw.rplidar("lidar")?; let imu = hw.i2c("imu")?; // Access config params from the handle let port: String = arm.params().get("port")?; let baud: u32 = arm.params().get("baudrate")?; let ids: Vec = arm.params().get("servo_ids")?; println!("Arm on {} @ {} baud, servos: {:?}", port, baud, ids); Ok(()) } ``` Available typed accessors: | Accessor | Hardware | |----------|----------| | `hw.dynamixel(name)` | Dynamixel servo bus | | `hw.rplidar(name)` | RPLidar scanner | | `hw.realsense(name)` | Intel RealSense camera | | `hw.i2c(name)` | I2C device | | `hw.serial(name)` | Serial/UART port | | `hw.can(name)` | CAN bus | | `hw.gpio(name)` | GPIO pin | | `hw.pwm(name)` | PWM output | | `hw.webcam(name)` | V4L2 camera | | `hw.input(name)` | Gamepad/input device | | `hw.bluetooth(name)` | Bluetooth LE device | | `hw.net(name)` | Network device (TCP/UDP) | | `hw.ethercat(name)` | EtherCAT bus | | `hw.raw(name)` | Any driver (escape hatch) | --- ## Step 4: Use Handles in Nodes Pass the `DriverHandle` to your node and read params in the constructor: ```rust use horus::prelude::*; use horus::drivers::{self, DriverHandle}; struct ArmController { servo_ids: Vec, port: String, cmd_topic: Topic, tick_count: u32, } impl ArmController { fn new(handle: DriverHandle) -> Result { let servo_ids: Vec = handle.params().get("servo_ids")?; let port: String = handle.params().get("port")?; println!("ArmController: {} servos on {}", servo_ids.len(), port); Ok(Self { servo_ids, port, cmd_topic: Topic::new("arm.command")?, tick_count: 0, }) } } impl Node for ArmController { fn name(&self) -> &str { "ArmController" } fn tick(&mut self) { self.tick_count += 1; // In a real driver, you'd read/write hardware here // using the port and servo_ids from config if let Some(cmd) = self.cmd_topic.recv() { println!("[Arm] Moving {} servos to target positions", self.servo_ids.len()); } } } fn main() -> Result<()> { let mut hw = drivers::load()?; let arm_handle = hw.dynamixel("arm")?; let mut scheduler = Scheduler::new().tick_rate(100_u64.hz()); scheduler.add(ArmController::new(arm_handle)?) .order(0) .rate(500_u64.hz()) .on_miss(Miss::SafeMode) .build()?; scheduler.run()?; Ok(()) } ``` --- ## Step 5: Custom Drivers with register_driver! For local code drivers (the `node = "..."` source), register your type so `HardwareSet::local()` can instantiate it from config: ```rust use horus::prelude::*; use horus::drivers::DriverParams; use horus::register_driver; struct ConveyorDriver { port: String, speed: f64, publisher: Topic, } impl ConveyorDriver { fn from_params(params: &DriverParams) -> Result { let port: String = params.get("port")?; let speed: f64 = params.get_or("speed", 1.0); Ok(Self { port, speed, publisher: Topic::new("conveyor.velocity")?, }) } } impl Node for ConveyorDriver { fn name(&self) -> &str { "ConveyorDriver" } fn tick(&mut self) { // Drive the conveyor at configured speed self.publisher.send(CmdVel::new(self.speed as f32, 0.0)); } } // Register so [drivers.conveyor] node = "ConveyorDriver" works register_driver!(ConveyorDriver, ConveyorDriver::from_params); ``` Then in `main.rs`: ```rust fn main() -> Result<()> { let mut hw = drivers::load()?; // local() looks up "ConveyorDriver" in the registry // and calls from_params() with the config params let conveyor_node = hw.local("conveyor")?; let mut scheduler = Scheduler::new().tick_rate(50_u64.hz()); // local() returns Box — no extra wrapping needed scheduler.add(conveyor_node).order(0).build()?; scheduler.run()?; Ok(()) } ``` The corresponding config: ```toml [drivers.conveyor] node = "ConveyorDriver" port = "/dev/ttyACM0" speed = 0.5 ``` --- ## Step 6: Testing Without Hardware Use `drivers::load_from()` to load from a test config file instead of the project's `horus.toml`: ```rust #[cfg(test)] mod tests { use super::*; use horus::drivers; #[test] fn arm_controller_from_config() { // Create a test config std::fs::write("test_drivers.toml", r#" [drivers.arm] terra = "dynamixel" port = "/dev/null" baudrate = 1000000 servo_ids = [1, 2, 3] "#).unwrap(); let mut hw = drivers::load_from("test_drivers.toml").unwrap(); let handle = hw.dynamixel("arm").unwrap(); let controller = ArmController::new(handle).unwrap(); assert_eq!(controller.servo_ids, vec![1, 2, 3]); std::fs::remove_file("test_drivers.toml").ok(); } } ``` ### DriverParams API Quick Reference | Method | Returns | Use | |--------|---------|-----| | `params.get::<T>(key)` | `Result<T>` | Required param (errors if missing) | | `params.get_or(key, default)` | `T` | Optional param with fallback | | `params.has(key)` | `bool` | Check if key exists | | `params.keys()` | `Iterator<&str>` | List all param names | | `params.raw(key)` | `Option<&toml::Value>` | Raw TOML value | Supported types for `get::<T>`: `String`, `bool`, `i32`, `i64`, `u8`, `u32`, `u64`, `f32`, `f64`, `Vec<T>`. --- ## Next Steps - [Driver API Reference](/rust/api/drivers) — full API documentation - [Real-Time Control](/tutorials/realtime-control) — scheduling drivers with RT guarantees - [Deployment](/operations) — deploying driver configs to a robot ======================================== # SECTION: Rust ======================================== --- ## Standard Messages Path: /rust/api/messages Description: Standard message types for robotics communication # Standard Messages Standard message types for robotics communication. All messages are designed for zero-copy shared memory transport. ```rust use horus::prelude::*; ``` --- ## Geometry Spatial primitives for position, orientation, and motion. ### Twist 3D velocity command with linear and angular components. ```rust pub struct Twist { pub linear: [f64; 3], // [x, y, z] in m/s pub angular: [f64; 3], // [roll, pitch, yaw] in rad/s pub timestamp_ns: u64, // Timestamp in nanoseconds since epoch } ``` #### Constructors ```rust // Full 3D velocity let twist = Twist::new([1.0, 0.0, 0.0], [0.0, 0.0, 0.5]); // 2D velocity (forward + rotation) let twist = Twist::new_2d(1.0, 0.5); // 1 m/s forward, 0.5 rad/s rotation // Stop command let twist = Twist::stop(); ``` #### Methods | Method | Description | |--------|-------------| | `is_valid()` | Returns true if all values are finite | --- ### Pose2D 2D pose (position and orientation) for planar robots. ```rust pub struct Pose2D { pub x: f64, // X position in meters pub y: f64, // Y position in meters pub theta: f64, // Orientation in radians pub timestamp_ns: u64, // Timestamp in nanoseconds since epoch } ``` #### Constructors ```rust let pose = Pose2D::new(1.0, 2.0, 0.5); // x=1m, y=2m, theta=0.5rad let pose = Pose2D::origin(); // (0, 0, 0) ``` #### Methods | Method | Description | |--------|-------------| | `distance_to(&other)` | Euclidean distance to another pose | | `normalize_angle()` | Normalize theta to [-π, π] | | `is_valid()` | Returns true if all values are finite | --- ### TransformStamped Timestamped 3D transformation (translation + quaternion rotation) for message passing. ```rust pub struct TransformStamped { pub translation: [f64; 3], // [x, y, z] in meters pub rotation: [f64; 4], // Quaternion [x, y, z, w] pub timestamp_ns: u64, } ``` #### Constructors ```rust let tf = TransformStamped::identity(); let tf = TransformStamped::new([1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]); let tf = TransformStamped::from_pose_2d(&pose); ``` #### Methods | Method | Description | |--------|-------------| | `is_valid()` | Check if quaternion is normalized | | `normalize_rotation()` | Normalize the quaternion | Note: For coordinate frame management (lookups, chains, interpolation), see [TransformFrame](/concepts/transform-frame) which uses its own `Transform` type. --- ### Point3 3D point in space. ```rust pub struct Point3 { pub x: f64, pub y: f64, pub z: f64, } ``` #### Methods ```rust let p1 = Point3::new(1.0, 2.0, 3.0); let p2 = Point3::origin(); let dist = p1.distance_to(&p2); ``` --- ### Vector3 3D vector for representing directions and velocities. ```rust pub struct Vector3 { pub x: f64, pub y: f64, pub z: f64, } ``` #### Methods | Method | Description | |--------|-------------| | `magnitude()` | Vector length | | `normalize()` | Normalize to unit vector | | `dot(&other)` | Dot product | | `cross(&other)` | Cross product | --- ### Quaternion Quaternion for 3D rotation representation. ```rust pub struct Quaternion { pub x: f64, pub y: f64, pub z: f64, pub w: f64, } ``` #### Constructors ```rust let q = Quaternion::identity(); let q = Quaternion::new(0.0, 0.0, 0.0, 1.0); let q = Quaternion::from_euler(roll, pitch, yaw); ``` --- ### Pose3D 3D pose with position and quaternion orientation. ```rust pub struct Pose3D { pub position: Point3, // Position in 3D space pub orientation: Quaternion, // Orientation as quaternion pub timestamp_ns: u64, } ``` #### Constructors ```rust let pose = Pose3D::new(Point3::new(1.0, 2.0, 0.5), Quaternion::identity()); let pose = Pose3D::identity(); let pose = Pose3D::from_pose_2d(&pose_2d); // Lift 2D pose into 3D ``` #### Methods | Method | Description | |--------|-------------| | `distance_to(&other)` | Euclidean distance to another pose | | `is_valid()` | Check if position is finite and quaternion is normalized | --- ### PoseStamped Pose with coordinate frame identifier. ```rust pub struct PoseStamped { pub pose: Pose3D, pub frame_id: [u8; 32], // Coordinate frame (null-terminated, max 31 chars) pub timestamp_ns: u64, } ``` #### Constructors ```rust let stamped = PoseStamped::new(); let stamped = PoseStamped::with_frame_id(pose, "map"); ``` #### Methods | Method | Description | |--------|-------------| | `frame_id_str()` | Get frame ID as `&str` | | `set_frame_id(id)` | Set frame ID string | --- ### Accel Linear and angular acceleration. ```rust pub struct Accel { pub linear: [f64; 3], // [x, y, z] in m/s² pub angular: [f64; 3], // [roll, pitch, yaw] in rad/s² pub timestamp_ns: u64, } ``` #### Constructors ```rust let accel = Accel::new([0.0, 0.0, 9.81], [0.0, 0.0, 0.0]); ``` #### Methods | Method | Description | |--------|-------------| | `is_valid()` | Check if all values are finite | --- ### AccelStamped Acceleration with coordinate frame. ```rust pub struct AccelStamped { pub accel: Accel, pub frame_id: [u8; 32], // Coordinate frame (null-terminated, max 31 chars) pub timestamp_ns: u64, } ``` #### Constructors ```rust let stamped = AccelStamped::new(); let stamped = AccelStamped::with_frame_id(accel, "imu_link"); ``` #### Methods | Method | Description | |--------|-------------| | `frame_id_str()` | Get frame ID as `&str` | | `set_frame_id(id)` | Set frame ID string | --- ### PoseWithCovariance Pose with 6x6 uncertainty covariance matrix. ```rust pub struct PoseWithCovariance { pub pose: Pose3D, pub covariance: [f64; 36], // 6x6 covariance (row-major): x, y, z, roll, pitch, yaw pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` #### Constructors ```rust let pose_cov = PoseWithCovariance::new(); let pose_cov = PoseWithCovariance::with_frame_id(pose, covariance, "map"); ``` #### Methods | Method | Description | |--------|-------------| | `position_variance()` | Get [x, y, z] variance from diagonal | | `orientation_variance()` | Get [roll, pitch, yaw] variance from diagonal | | `frame_id_str()` | Get frame ID as `&str` | | `set_frame_id(id)` | Set frame ID string | #### Example ```rust // EKF localization output with uncertainty let mut pose_cov = PoseWithCovariance::new(); pose_cov.pose = estimated_pose; // Diagonal: position uncertainty 0.1m, orientation 0.05rad pose_cov.covariance[0] = 0.01; // x variance pose_cov.covariance[7] = 0.01; // y variance pose_cov.covariance[14] = 0.04; // z variance pose_cov.covariance[21] = 0.0025; // roll variance pose_cov.covariance[28] = 0.0025; // pitch variance pose_cov.covariance[35] = 0.0025; // yaw variance topic.send(pose_cov); ``` --- ### TwistWithCovariance Velocity with 6x6 uncertainty covariance matrix. ```rust pub struct TwistWithCovariance { pub twist: Twist, pub covariance: [f64; 36], // 6x6 covariance (row-major): vx, vy, vz, wx, wy, wz pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` #### Constructors ```rust let twist_cov = TwistWithCovariance::new(); let twist_cov = TwistWithCovariance::with_frame_id(twist, covariance, "base_link"); ``` #### Methods | Method | Description | |--------|-------------| | `linear_variance()` | Get [vx, vy, vz] variance from diagonal | | `angular_variance()` | Get [wx, wy, wz] variance from diagonal | | `frame_id_str()` | Get frame ID as `&str` | | `set_frame_id(id)` | Set frame ID string | --- ## Sensor Standard sensor data formats. ### LaserScan 2D LiDAR scan data with 360 range measurements. ```rust pub struct LaserScan { pub ranges: [f32; 360], // Range measurements in meters pub angle_min: f32, // Start angle in radians pub angle_max: f32, // End angle in radians pub range_min: f32, // Minimum valid range pub range_max: f32, // Maximum valid range pub angle_increment: f32, // Angular resolution pub time_increment: f32, // Time between measurements pub scan_time: f32, // Full scan time pub timestamp_ns: u64, } ``` #### Constructors ```rust let scan = LaserScan::new(); let scan = LaserScan::default(); ``` #### Methods | Method | Description | |--------|-------------| | `angle_at(index)` | Get angle for range index | | `is_range_valid(index)` | Check if reading is valid | | `valid_count()` | Count valid readings | | `min_range()` | Get minimum valid range | #### Example ```rust if let Some(scan) = scan_sub.recv() { if let Some(min_dist) = scan.min_range() { if min_dist < 0.5 { // Obstacle detected! } } } ``` --- ### Imu IMU sensor data (orientation, angular velocity, acceleration). ```rust pub struct Imu { pub orientation: [f64; 4], // Quaternion [x, y, z, w] pub orientation_covariance: [f64; 9], // 3x3 covariance (-1 = no data) pub angular_velocity: [f64; 3], // [x, y, z] in rad/s pub angular_velocity_covariance: [f64; 9], pub linear_acceleration: [f64; 3], // [x, y, z] in m/s² pub linear_acceleration_covariance: [f64; 9], pub timestamp_ns: u64, } ``` #### Constructors ```rust let imu = Imu::new(); ``` #### Methods | Method | Description | |--------|-------------| | `set_orientation_from_euler(roll, pitch, yaw)` | Set orientation from Euler angles | | `has_orientation()` | Check if orientation data is available | | `is_valid()` | Check if all values are finite | | `angular_velocity_vec()` | Get angular velocity as Vector3 | | `linear_acceleration_vec()` | Get linear acceleration as Vector3 | --- ### Odometry Combined pose and velocity estimate. ```rust pub struct Odometry { pub pose: Pose2D, pub twist: Twist, pub pose_covariance: [f64; 36], // 6x6 covariance pub twist_covariance: [f64; 36], pub frame_id: [u8; 32], // e.g., "odom" pub child_frame_id: [u8; 32], // e.g., "base_link" pub timestamp_ns: u64, } ``` #### Methods | Method | Description | |--------|-------------| | `set_frames(frame, child)` | Set frame IDs | | `update(pose, twist)` | Update pose and velocity | | `is_valid()` | Check validity | --- ### NavSatFix GPS/GNSS position data. ```rust pub struct NavSatFix { pub latitude: f64, // Degrees (+ North, - South) pub longitude: f64, // Degrees (+ East, - West) pub altitude: f64, // Meters above WGS84 pub position_covariance: [f64; 9], pub position_covariance_type: u8, pub status: u8, // Fix status pub satellites_visible: u16, pub hdop: f32, pub vdop: f32, pub speed: f32, // m/s pub heading: f32, // degrees pub timestamp_ns: u64, } ``` #### Constants ```rust NavSatFix::STATUS_NO_FIX // 0 NavSatFix::STATUS_FIX // 1 NavSatFix::STATUS_SBAS_FIX // 2 NavSatFix::STATUS_GBAS_FIX // 3 ``` #### Methods | Method | Description | |--------|-------------| | `from_coordinates(lat, lon, alt)` | Create from coordinates | | `has_fix()` | Check if GPS has fix | | `is_valid()` | Check coordinate validity | | `horizontal_accuracy()` | Estimated accuracy in meters | | `distance_to(&other)` | Distance to another position (Haversine) | --- ### BatteryState Battery status information. ```rust pub struct BatteryState { pub voltage: f32, // Volts pub current: f32, // Amperes (negative = discharging) pub charge: f32, // Amp-hours pub capacity: f32, // Amp-hours pub percentage: f32, // 0-100 pub power_supply_status: u8, pub temperature: f32, // Celsius pub cell_voltages: [f32; 16], pub cell_count: u8, pub timestamp_ns: u64, } ``` #### Constants ```rust BatteryState::STATUS_UNKNOWN // 0 BatteryState::STATUS_CHARGING // 1 BatteryState::STATUS_DISCHARGING // 2 BatteryState::STATUS_FULL // 3 ``` #### Methods | Method | Description | |--------|-------------| | `new(voltage, percentage)` | Create new battery state | | `is_low(threshold)` | Check if below threshold | | `is_critical()` | Check if below 10% | | `time_remaining()` | Estimated time in seconds | --- ### RangeSensor Single-point distance measurement (ultrasonic, IR). ```rust pub struct RangeSensor { pub sensor_type: u8, // 0=ultrasonic, 1=infrared pub field_of_view: f32, // radians pub min_range: f32, // meters pub max_range: f32, // meters pub range: f32, // meters pub timestamp_ns: u64, } ``` --- ### JointState Joint positions, velocities, and efforts for multi-joint robots (up to 16 joints). ```rust pub struct JointState { pub names: [[u8; 32]; 16], // Joint names (null-terminated, max 31 chars each) pub joint_count: u8, // Number of valid joints pub positions: [f64; 16], // Radians (revolute) or meters (prismatic) pub velocities: [f64; 16], // rad/s or m/s pub efforts: [f64; 16], // Torque (Nm) or force (N) pub timestamp_ns: u64, } ``` #### Methods | Method | Description | |--------|-------------| | `new()` | Create empty joint state | | `add_joint(name, pos, vel, effort)` | Add a joint entry | | `joint_name(index)` | Get joint name as `&str` | | `position(index)` | Get position for joint | | `velocity(index)` | Get velocity for joint | | `effort(index)` | Get effort for joint | #### Example ```rust let mut joints = JointState::new(); joints.add_joint("shoulder", 0.5, 0.0, 1.2); joints.add_joint("elbow", -0.8, 0.1, 0.5); joints.add_joint("wrist", 0.2, 0.0, 0.1); joint_topic.send(joints); ``` --- ### Temperature Temperature reading in Celsius. ```rust pub struct Temperature { pub temperature: f64, // Degrees Celsius pub variance: f64, // Variance (0 if exact) pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` #### Constructors ```rust let temp = Temperature::new(); let temp = Temperature::with_frame_id(22.5, 0.1, "motor_0"); ``` --- ### FluidPressure Pressure reading in Pascals. ```rust pub struct FluidPressure { pub fluid_pressure: f64, // Pressure in Pascals pub variance: f64, // Variance (0 if exact) pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` #### Constructors ```rust let pressure = FluidPressure::new(); let pressure = FluidPressure::with_frame_id(101325.0, 10.0, "barometer"); ``` --- ### Illuminance Light level in Lux. ```rust pub struct Illuminance { pub illuminance: f64, // Illuminance in Lux pub variance: f64, // Variance (0 if exact) pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` #### Constructors ```rust let lux = Illuminance::new(); let lux = Illuminance::with_frame_id(500.0, 5.0, "ambient_sensor"); ``` --- ### MagneticField Magnetic field vector in Tesla with 3x3 covariance. ```rust pub struct MagneticField { pub magnetic_field: [f64; 3], // [x, y, z] in Tesla pub magnetic_field_covariance: [f64; 9], // 3x3 covariance (row-major). [0] = -1 means unknown pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` #### Constructors ```rust let mag = MagneticField::new(); let mag = MagneticField::with_frame_id([25e-6, 0.0, -45e-6], "imu_link"); ``` --- ## Vision Image and camera data types. ### Image Pool-backed RAII camera image type with zero-copy shared memory transport. Fields are private — use accessor methods. ```rust // Image is an RAII type, not a plain struct. // Create with Image::new(width, height, encoding)? // Access data with .data(), .pixel(), etc. // See Vision Messages for full API. let mut img = Image::new(640, 480, ImageEncoding::Rgb8)?; img.copy_from(&pixel_data); ``` #### ImageEncoding ```rust pub enum ImageEncoding { Rgb8, Bgr8, Rgba8, Bgra8, Mono8, Mono16, Yuv422, Mono32F, Rgb32F, BayerRggb8, Depth16, } ``` --- ### CameraInfo Camera calibration information. ```rust pub struct CameraInfo { pub width: u32, pub height: u32, pub distortion_model: [u8; 16], pub distortion_coefficients: [f64; 8], // [k1, k2, p1, p2, k3, k4, k5, k6] pub camera_matrix: [f64; 9], // Intrinsic matrix (3x3) pub rectification_matrix: [f64; 9], // Rectification (3x3) pub projection_matrix: [f64; 12], // Projection (3x4) } ``` --- ### RegionOfInterest Region of interest for image cropping and processing. ```rust pub struct RegionOfInterest { pub x_offset: u32, pub y_offset: u32, pub width: u32, pub height: u32, pub do_rectify: bool, } ``` #### Methods | Method | Description | |--------|-------------| | `new(x, y, w, h)` | Create a new ROI | | `contains(x, y)` | Check if point is inside ROI | | `area()` | Get area in pixels | --- ### CompressedImage Compressed image data (JPEG, PNG, WebP). ```rust pub struct CompressedImage { pub format: [u8; 8], // "jpeg", "png", "webp" pub data: Vec, // Compressed image bytes pub width: u32, pub height: u32, pub frame_id: [u8; 32], pub timestamp_ns: u64, } ``` Note: `CompressedImage` uses `Vec` for variable-length data, so it uses MessagePack serialization instead of zero-copy. --- ### Detection Object detection result (2D). ```rust pub struct Detection { pub bbox: BoundingBox2D, pub confidence: f32, pub class_id: u32, pub class_name: [u8; 32], pub instance_id: u32, } ``` --- ### BoundingBox2D 2D bounding box in pixel coordinates. ```rust pub struct BoundingBox2D { pub x: f32, // Top-left X (pixels) pub y: f32, // Top-left Y (pixels) pub width: f32, // Width (pixels) pub height: f32, // Height (pixels) } ``` #### Methods | Method | Description | |--------|-------------| | `new(x, y, w, h)` | Create from top-left corner | | `from_center(cx, cy, w, h)` | Create from center point | | `center_x()` / `center_y()` | Get center coordinates | | `area()` | Get area in pixels² | | `iou(&other)` | Intersection over Union | --- ### BoundingBox3D 3D bounding box in world coordinates. ```rust pub struct BoundingBox3D { pub cx: f32, // Center X (meters) pub cy: f32, // Center Y (meters) pub cz: f32, // Center Z (meters) pub length: f32, // Along X axis (meters) pub width: f32, // Along Y axis (meters) pub height: f32, // Along Z axis (meters) pub roll: f32, // Rotation around X (radians) pub pitch: f32, // Rotation around Y (radians) pub yaw: f32, // Rotation around Z (radians) } ``` #### Methods | Method | Description | |--------|-------------| | `new(cx, cy, cz, l, w, h)` | Create axis-aligned box | | `with_rotation(cx, cy, cz, l, w, h, roll, pitch, yaw)` | Create rotated box | | `volume()` | Get volume in m³ | --- ### Detection3D 3D object detection result with optional velocity tracking. ```rust pub struct Detection3D { pub bbox: BoundingBox3D, pub confidence: f32, pub class_id: u32, pub class_name: [u8; 32], pub velocity_x: f32, // m/s (for tracking) pub velocity_y: f32, pub velocity_z: f32, pub instance_id: u32, } ``` #### Methods | Method | Description | |--------|-------------| | `new(bbox, confidence, class_id)` | Create detection | | `with_velocity(vx, vy, vz)` | Add velocity estimate | | `class_name()` | Get class name as `&str` | | `set_class_name(name)` | Set class name string | --- ## Control Actuator command messages. ### MotorCommand Motor control command. ```rust pub struct MotorCommand { pub motor_id: u8, pub mode: u8, // 0=velocity, 1=position, 2=torque, 3=voltage pub target: f64, pub max_velocity: f64, pub max_acceleration: f64, pub feed_forward: f64, pub enable: u8, pub timestamp_ns: u64, } ``` --- ### ServoCommand Servo position command. ```rust pub struct ServoCommand { pub servo_id: u8, pub position: f32, // radians pub speed: f32, // 0-1 (0 = max speed) pub enable: u8, pub timestamp_ns: u64, } ``` --- ### PidConfig PID controller configuration. ```rust pub struct PidConfig { pub controller_id: u8, pub kp: f64, pub ki: f64, pub kd: f64, pub integral_limit: f64, pub output_limit: f64, pub anti_windup: u8, pub timestamp_ns: u64, } ``` --- ### CmdVel Simple 2D velocity command — the **preferred type for ground robots**. ```rust pub struct CmdVel { pub timestamp_ns: u64, pub linear: f32, // Forward velocity in m/s pub angular: f32, // Turning velocity in rad/s } ``` #### Constructors ```rust let cmd = CmdVel::new(0.5, 0.2); // 0.5 m/s forward, 0.2 rad/s left turn let cmd = CmdVel::zero(); // Stop ``` Converts to/from `Twist`: ```rust let twist: Twist = cmd.into(); let cmd = CmdVel::from(twist); ``` --- ### DifferentialDriveCommand Left/right wheel velocity command for 2-wheel robots. ```rust pub struct DifferentialDriveCommand { pub left_velocity: f64, // Left wheel in rad/s pub right_velocity: f64, // Right wheel in rad/s pub max_acceleration: f64, // Maximum acceleration in rad/s² pub enable: u8, // Enable motors pub timestamp_ns: u64, } ``` #### Constructors ```rust let cmd = DifferentialDriveCommand::new(5.0, 5.0); // Both wheels forward let cmd = DifferentialDriveCommand::stop(); // Emergency stop let cmd = DifferentialDriveCommand::from_twist(&twist, wheel_separation); ``` #### Methods | Method | Description | |--------|-------------| | `is_valid()` | Check if velocities are finite | --- ### TrajectoryPoint Waypoint in a trajectory with position, velocity, acceleration, and timing. ```rust pub struct TrajectoryPoint { pub position: [f64; 3], // [x, y, z] pub velocity: [f64; 3], // [vx, vy, vz] pub acceleration: [f64; 3], // [ax, ay, az] pub orientation: [f64; 4], // Quaternion [x, y, z, w] pub angular_velocity: [f64; 3], // [wx, wy, wz] pub time_from_start: f64, // Seconds from trajectory start } ``` #### Constructors ```rust let point = TrajectoryPoint::new_2d(1.0, 2.0, 0.5); // x, y, theta let point = TrajectoryPoint::stationary([1.0, 2.0, 0.0]); ``` --- ## GenericMessage Dynamic message type for cross-language communication. Uses a fixed-size buffer (4KB max payload) with MessagePack serialization, making it safe for shared memory transport. ```rust pub struct GenericMessage { pub inline_data: [u8; 256], // First 256 bytes (inline) pub overflow_data: [u8; 3840], // Overflow up to 3840 more bytes pub metadata: [u8; 256], // Optional metadata // ... internal length tracking fields } ``` Total maximum payload: 4,096 bytes (`inline_data` + `overflow_data`). ### Constructors ```rust // From raw bytes (returns Err if > 4KB) let msg = GenericMessage::new(data_vec)?; // From any serializable type (uses MessagePack) let msg = GenericMessage::from_value(&my_struct)?; // With metadata string (max 255 chars) let msg = GenericMessage::with_metadata(data, "my_type".to_string())?; ``` ### Methods | Method | Return Type | Description | |--------|-------------|-------------| | `data()` | `Vec` | Get payload bytes | | `metadata()` | `Option` | Get metadata string if present | | `to_value::()` | `Result` | Deserialize from MessagePack to typed value | ### Example ```rust use std::collections::HashMap; // Send any serializable data let mut data = HashMap::new(); data.insert("x", 1.0); data.insert("y", 2.0); let msg = GenericMessage::from_value(&data)?; topic.send(msg); // Receive and deserialize if let Some(msg) = topic.recv() { let data: HashMap = msg.to_value()?; println!("x: {}", data["x"]); } ``` Use typed messages (e.g., `Twist`, `Pose2D`) instead of `GenericMessage` whenever possible — they are faster (zero-copy) and type-safe. --- ## Perception Types for computer vision, landmark detection, and semantic understanding. ### Landmark 2D landmark point (keypoint, fiducial marker). ```rust pub struct Landmark { pub x: f32, // X coordinate (pixels or normalized 0-1) pub y: f32, // Y coordinate (pixels or normalized 0-1) pub visibility: f32, // Confidence (0.0 - 1.0) pub index: u32, // Landmark ID (e.g., 0=nose, 1=left_eye) } ``` #### Methods | Method | Description | |--------|-------------| | `new(x, y, visibility, index)` | Create landmark | | `visible(x, y, index)` | Create with visibility = 1.0 | | `is_visible()` | Check if visibility > 0 | | `distance_to(&other)` | Euclidean distance | --- ### Landmark3D 3D landmark point with depth. ```rust pub struct Landmark3D { pub x: f32, // X (meters or normalized) pub y: f32, // Y (meters or normalized) pub z: f32, // Z (depth) pub visibility: f32, // Confidence (0.0 - 1.0) pub index: u32, // Landmark ID } ``` #### Methods | Method | Description | |--------|-------------| | `new(x, y, z, visibility, index)` | Create 3D landmark | | `to_2d()` | Project to 2D Landmark (drops z) | | `distance_to(&other)` | 3D Euclidean distance | --- ### LandmarkArray Collection of landmarks for a single detection (pose, face, hand). ```rust pub struct LandmarkArray { pub num_landmarks: u32, pub dimension: u32, // 2 for 2D, 3 for 3D pub instance_id: u32, // Person/detection ID pub confidence: f32, // Overall pose confidence (0.0 - 1.0) pub timestamp_ns: u64, pub bbox_x: f32, // Bounding box X (pixels) pub bbox_y: f32, // Bounding box Y (pixels) pub bbox_width: f32, pub bbox_height: f32, } ``` #### Constructors ```rust let pose = LandmarkArray::coco_pose(); // 17 keypoints (COCO format) let pose = LandmarkArray::mediapipe_pose(); // 33 keypoints let hand = LandmarkArray::mediapipe_hand(); // 21 keypoints let face = LandmarkArray::mediapipe_face(); // 468 keypoints ``` --- ### PlaneDetection Detected plane surface (floor, wall, table). ```rust pub struct PlaneDetection { pub coefficients: [f64; 4], // [a, b, c, d] where ax + by + cz + d = 0 pub center: Point3, // Center of the plane pub normal: Vector3, // Normal vector pub size: [f64; 2], // [width, height] if bounded pub inlier_count: u32, // Number of inlier points pub confidence: f32, // Detection confidence (0.0 - 1.0) pub plane_type: [u8; 16], // "floor", "wall", "table", etc. pub timestamp_ns: u64, } ``` #### Methods | Method | Description | |--------|-------------| | `new()` | Create empty plane | | `distance_to_point(&point)` | Signed distance from point to plane | | `contains_point(&point)` | Check if point is within plane bounds | | `with_type(label)` | Set plane type label | | `plane_type_str()` | Get plane type as `&str` | --- ### SegmentationMask Pixel-level semantic/instance segmentation. ```rust pub struct SegmentationMask { pub width: u32, pub height: u32, pub num_classes: u32, // Number of semantic classes pub mask_type: u32, // 0=semantic, 1=instance, 2=panoptic pub timestamp_ns: u64, pub seq: u64, pub frame_id: [u8; 32], } ``` #### Constructors ```rust let mask = SegmentationMask::semantic(640, 480, 21); // 21 classes let mask = SegmentationMask::instance(640, 480, 80); // 80 instance classes let mask = SegmentationMask::panoptic(640, 480, 133); // Panoptic segmentation ``` #### Methods | Method | Description | |--------|-------------| | `is_semantic()` | Check if semantic segmentation | | `is_instance()` | Check if instance segmentation | | `is_panoptic()` | Check if panoptic segmentation | | `data_size()` | Required buffer size for u8 labels | | `data_size_u16()` | Required buffer size for u16 labels | --- ## Navigation Path planning and goal management. ### NavGoal Navigation goal with target pose and tolerances. ```rust pub struct NavGoal { pub target_pose: Pose2D, pub tolerance_position: f64, // Position tolerance in meters pub tolerance_angle: f64, // Orientation tolerance in radians pub timeout_seconds: f64, // Max time to reach goal (0 = no limit) pub priority: u8, // Goal priority (0 = highest) pub goal_id: u32, // Unique goal identifier pub timestamp_ns: u64, } ``` #### Constructors ```rust let goal = NavGoal::new(Pose2D::new(5.0, 3.0, 0.0)); let goal = NavGoal::new(target).with_timeout(30.0).with_priority(1); ``` #### Methods | Method | Description | |--------|-------------| | `is_position_reached(¤t)` | Check if position is within tolerance | | `is_orientation_reached(¤t)` | Check if angle is within tolerance | | `is_reached(¤t)` | Check if both position and angle are reached | #### Example ```rust let goal = NavGoal::new(Pose2D::new(5.0, 3.0, 1.57)); goal_topic.send(goal); // In planner node if let Some(goal) = goal_topic.recv() { if goal.is_reached(¤t_pose) { hlog!(info, "Goal {} reached!", goal.goal_id); } } ``` --- ### NavPath Ordered sequence of waypoints (up to 256). ```rust pub struct NavPath { pub waypoints: [Waypoint; 256], // Waypoint = {pose, velocity, time_from_start, curvature, stop_required} pub waypoint_count: u16, pub total_length: f64, // Total path length in meters pub duration_seconds: f64, // Estimated completion time pub frame_id: [u8; 32], pub algorithm: [u8; 32], // e.g., "a_star", "rrt" pub timestamp_ns: u64, } ``` #### Methods | Method | Description | |--------|-------------| | `new()` | Create empty path | | `add_waypoint(waypoint)` | Append a waypoint | | `waypoints()` | Get slice of valid waypoints | | `closest_waypoint_index(&pose)` | Find nearest waypoint to pose | | `calculate_progress(&pose)` | Get progress along path (0.0 - 1.0) | | `frame_id_str()` | Get frame ID as `&str` | --- ## Diagnostics System health, monitoring, and diagnostic data. ### DiagnosticValue Typed key-value pair for diagnostic data. ```rust pub struct DiagnosticValue { pub key: [u8; 32], // Key name (null-terminated) pub value: [u8; 64], // Value as string (null-terminated) pub value_type: u8, // 0=string, 1=int, 2=float, 3=bool } ``` #### Constructors ```rust let val = DiagnosticValue::string("firmware", "v2.1.0"); let val = DiagnosticValue::int("error_count", 3); let val = DiagnosticValue::float("temperature", 45.2); let val = DiagnosticValue::bool("calibrated", true); ``` --- ### DiagnosticReport Component health report with up to 16 key-value entries. ```rust pub struct DiagnosticReport { pub component: [u8; 32], pub values: [DiagnosticValue; 16], pub value_count: u8, pub level: u8, // Overall status level pub timestamp_ns: u64, } ``` #### Methods | Method | Description | |--------|-------------| | `new(component)` | Create empty report | | `add_value(value)` | Add a diagnostic value | | `add_string(key, val)` | Add string entry | | `add_int(key, val)` | Add integer entry | | `add_float(key, val)` | Add float entry | | `add_bool(key, val)` | Add boolean entry | | `set_level(level)` | Set status level | #### Example ```rust let mut report = DiagnosticReport::new("left_motor"); report.add_float("temperature", 42.5); report.add_int("error_count", 0); report.add_bool("calibrated", true); report.add_string("firmware", "v2.1.0"); report.set_level(0); // OK diag_topic.send(report); ``` --- ### NodeHeartbeat Periodic node health signal (written via filesystem, not Topic). ```rust pub struct NodeHeartbeat { pub state: u8, // Node execution state pub health: u8, // Health status pub tick_count: u64, pub target_rate_hz: u32, pub actual_rate_hz: u32, pub error_count: u32, pub last_tick_timestamp: u64, // Unix epoch seconds pub heartbeat_timestamp: u64, // Unix epoch seconds } ``` #### Methods | Method | Description | |--------|-------------| | `new()` | Create default heartbeat | | `update_timestamp()` | Set heartbeat_timestamp to now | | `is_fresh(max_age_secs)` | Check if heartbeat is recent | | `to_bytes()` / `from_bytes()` | Serialize for filesystem storage | --- ## Fixed-Size Types and Zero-Copy Most HORUS message types are fixed-size with no heap allocation, enabling zero-copy shared memory transport at ~50ns latency. **Fixed-size types** (94% of all messages): No heap allocation, zero-copy capable. **Variable-size types**: Use `Vec` for variable-size data — serialized via MessagePack. | Category | Fixed-Size / Total | Variable-Size Types | |----------|-------------|---------------| | Geometry | 12/12 | — | | Sensor | 11/11 | — | | Control | 9/9 | — | | Vision | 5/7 | `Image` (pool-backed RAII), `CompressedImage` | | Detection | 4/4 | — | | Perception | 4/6 | `PointCloud` (pool-backed RAII), `DepthImage` (pool-backed RAII) | | Navigation | 9/11 | `OccupancyGrid`, `CostMap` | | Diagnostics | 11/11 | — | | Force/Haptics | 5/5 | — | Backend detection is automatic — you don't need to configure anything. HORUS checks at topic creation time whether your type is fixed-size and selects the optimal backend. ### Size Reference | Message | Size | Zero-Copy | |---------|------|-----| | `CmdVel` | 16 bytes | Yes | | `Twist` | 56 bytes | Yes | | `Pose2D` | 32 bytes | Yes | | `Pose3D` | 72 bytes | Yes | | `Imu` | ~200 bytes | Yes | | `JointState` | ~1.5 KB | Yes | | `LaserScan` | ~1.5 KB | Yes | | `NavPath` | ~32 KB | Yes | | `Image` | Pool-backed RAII | Descriptor is fixed-size | | `GenericMessage` | ~4.5 KB | Yes | --- ## Detailed Message Documentation For comprehensive documentation of specialized message types, see: | Category | Description | |----------|-------------| | [Vision Messages](/rust/api/vision-messages) | Image, CameraInfo, Detection, CompressedImage | | [Perception Messages](/rust/api/perception-messages) | PointCloud, DepthImage, BoundingBox3D, PlaneDetection | | [Control Messages](/rust/api/control-messages) | MotorCommand, ServoCommand, PidConfig, JointCommand | | [Force & Haptic Messages](/rust/api/force-messages) | WrenchStamped, ImpedanceParameters, ForceCommand | | [Navigation Messages](/rust/api/navigation-messages) | Goal, Path, OccupancyGrid, CostMap, VelocityObstacle | | [Diagnostics Messages](/rust/api/diagnostics-messages) | Heartbeat, DiagnosticStatus, EmergencyStop, ResourceUsage, SafetyStatus | | [Input Messages](/rust/api/input-messages) | KeyboardInput, JoystickInput for teleoperation and HID control | | [Clock & Time Messages](/rust/api/clock-messages) | Clock, TimeReference for simulation time and synchronization | | [TensorPool API](/rust/api/tensor-pool) | Zero-copy tensor memory management | ## Python Equivalents All message types above are available in Python with identical fields. See: - [Python Message Library](/python/library/python-message-library) — 55+ message types with Python examples - [Python Memory Types](/python/api/memory-types) — Image, PointCloud, DepthImage (pool-backed) - [Python Perception](/python/api/perception) — DetectionList, TrackedObject, COCOPose --- ## Advanced Examples Path: /rust/examples/advanced-examples Description: Complex patterns, multi-process systems, and Python integration # Advanced Examples Advanced HORUS patterns for complex robotics systems. These examples demonstrate priority-based safety systems, multi-process architectures, and cross-language communication. **Prerequisites**: Complete [Basic Examples](/rust/examples/basic-examples) first. --- ## 1. State Machine Node Implement complex behavior using state machines - ideal for autonomous robots with multiple operating modes. **File: `state_machine.rs`** ```rust use horus::prelude::*; #[derive(Debug, Clone, Copy, PartialEq)] enum RobotState { Idle, Moving, ObstacleDetected, Rotating, Escaped, } struct StateMachineNode { state: RobotState, obstacle_sub: Topic, cmd_pub: Topic, rotation_counter: u32, } impl StateMachineNode { fn new() -> Result { Ok(Self { state: RobotState::Idle, obstacle_sub: Topic::new("obstacle_detected")?, cmd_pub: Topic::new("cmd_vel")?, rotation_counter: 0, }) } } impl Node for StateMachineNode { fn name(&self) -> &str { "StateMachineNode" } fn init(&mut self) -> Result<()> { hlog!(info, "State machine initialized - starting in IDLE state"); Ok(()) } fn tick(&mut self) { // Check for obstacles let obstacle = self.obstacle_sub.recv().unwrap_or(false); // Store previous state for logging let prev_state = self.state; // State machine logic self.state = match self.state { RobotState::Idle => { if !obstacle { RobotState::Moving } else { RobotState::Idle } } RobotState::Moving => { if obstacle { self.cmd_pub.send(CmdVel::zero()); // Stop RobotState::ObstacleDetected } else { self.cmd_pub.send(CmdVel::new(1.0, 0.0)); // Forward RobotState::Moving } } RobotState::ObstacleDetected => { self.rotation_counter = 0; RobotState::Rotating } RobotState::Rotating => { self.cmd_pub.send(CmdVel::new(0.0, 0.5)); // Rotate self.rotation_counter += 1; if self.rotation_counter > 50 { RobotState::Escaped } else { RobotState::Rotating } } RobotState::Escaped => { RobotState::Moving // Resume moving } }; // Log state transitions if self.state != prev_state { hlog!(info, "State transition: {:?} -> {:?}", prev_state, self.state); } } fn shutdown(&mut self) -> Result<()> { // Ensure robot is stopped self.cmd_pub.send(CmdVel::zero()); hlog!(info, "State machine shutdown"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(StateMachineNode::new()?).order(0).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run state_machine.rs ``` **Key Concepts**: - Enum for states: `Idle`, `Moving`, `ObstacleDetected`, `Rotating`, `Escaped` - Match expression handles state transitions - Each state defines behavior and next state - Log state transitions for debugging --- ## 2. Priority-Based Safety System Use node priorities to ensure safety-critical tasks always run first - essential for production robotics. **File: `safety_system.rs`** ```rust use horus::prelude::*; // CRITICAL PRIORITY: Emergency stop struct EmergencyStopNode { battery_sub: Topic, lidar_sub: Topic, // Min obstacle distance estop_pub: Topic, estop_active: bool, } impl EmergencyStopNode { fn new() -> Result { Ok(Self { battery_sub: Topic::new("battery_state")?, lidar_sub: Topic::new("min_distance")?, estop_pub: Topic::new("emergency_stop")?, estop_active: false, }) } } impl Node for EmergencyStopNode { fn name(&self) -> &str { "EmergencyStop" } fn init(&mut self) -> Result<()> { hlog!(info, " Emergency stop system online - CRITICAL priority"); Ok(()) } fn tick(&mut self) { let mut should_stop = false; // Check battery if let Some(battery) = self.battery_sub.recv() { if battery.is_critical() { // Below 10% should_stop = true; hlog!(error, " CRITICAL: Battery at {:.0}% - EMERGENCY STOP!", battery.percentage); } } // Check obstacle distance if let Some(min_dist) = self.lidar_sub.recv() { if min_dist < 0.2 { // 20cm should_stop = true; hlog!(error, " CRITICAL: Obstacle at {:.2}m - EMERGENCY STOP!", min_dist); } } // Publish estop state if should_stop != self.estop_active { self.estop_pub.send(should_stop); self.estop_active = should_stop; } } fn shutdown(&mut self) -> Result<()> { // Always activate estop on shutdown self.estop_pub.send(true); hlog!(warn, "Emergency stop system offline"); Ok(()) } } // HIGH PRIORITY: Motor controller struct MotorController { estop_sub: Topic, cmd_sub: Topic, motor_pub: Topic, estop_active: bool, } impl MotorController { fn new() -> Result { Ok(Self { estop_sub: Topic::new("emergency_stop")?, cmd_sub: Topic::new("cmd_vel_request")?, motor_pub: Topic::new("cmd_vel_actual")?, estop_active: false, }) } } impl Node for MotorController { fn name(&self) -> &str { "MotorController" } fn init(&mut self) -> Result<()> { hlog!(info, "Motor controller online - HIGH priority"); Ok(()) } fn tick(&mut self) { // Check emergency stop FIRST if let Some(estop) = self.estop_sub.recv() { if estop != self.estop_active { self.estop_active = estop; if estop { hlog!(warn, "Motors DISABLED - emergency stop active"); } else { hlog!(info, "Motors ENABLED - emergency stop cleared"); } } } // Don't move if estop active if self.estop_active { self.motor_pub.send(CmdVel::zero()); return; } // Process normal commands if let Some(cmd) = self.cmd_sub.recv() { self.motor_pub.send(cmd); hlog!(debug, "Motors: linear={:.2}, angular={:.2}", cmd.linear, cmd.angular); } } fn shutdown(&mut self) -> Result<()> { // Stop motors self.motor_pub.send(CmdVel::zero()); hlog!(info, "Motor controller stopped"); Ok(()) } } // BACKGROUND PRIORITY: Data logging struct LoggerNode { cmd_sub: Topic, battery_sub: Topic, } impl LoggerNode { fn new() -> Result { Ok(Self { cmd_sub: Topic::new("cmd_vel_actual")?, battery_sub: Topic::new("battery_state")?, }) } } impl Node for LoggerNode { fn name(&self) -> &str { "Logger" } fn init(&mut self) -> Result<()> { hlog!(info, " Logger online - BACKGROUND priority"); Ok(()) } fn tick(&mut self) { // Log velocity commands if let Some(cmd) = self.cmd_sub.recv() { hlog!(debug, "LOG: cmd_vel({:.2}, {:.2})", cmd.linear, cmd.angular); } // Log battery state if let Some(battery) = self.battery_sub.recv() { hlog!(debug, "LOG: battery({:.1}V, {:.0}%)", battery.voltage, battery.percentage); } // In production: write to file, database, etc. } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Logger stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Order 0 (Critical): Safety runs FIRST scheduler.add(EmergencyStopNode::new()?).order(0).build()?; // Order 1 (High): Control runs SECOND scheduler.add(MotorController::new()?).order(1).build()?; // Order 4 (Background): Logging runs LAST scheduler.add(LoggerNode::new()?).order(4).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run safety_system.rs ``` **Key Concepts**: - **Priority 0 (Critical)**: Emergency stop - runs first, always - **Priority 1 (High)**: Motor control - runs after safety checks - **Priority 4 (Background)**: Logging - runs last, non-critical - Lower number = higher priority - Safety systems should always check estop before acting --- ## 3. Python Multi-Process System Build a complete sensor monitoring system with Python nodes running as independent processes. ### Project Structure ```bash mkdir multi_node_system cd multi_node_system mkdir nodes ``` ### Sensor Node **nodes/sensor.py:** ```python #!/usr/bin/env python3 import horus import random def sensor_tick(node): # Generate realistic temperature with noise temperature = 20.0 + random.random() * 10.0 node.send("temperature", temperature) print(f"Sensor: {temperature:.1f}°C") sensor = horus.Node(name="SensorNode", tick=sensor_tick, order=0, rate=2, pubs=["temperature"]) horus.run(sensor) ``` ### Controller Node **nodes/controller.py:** ```python #!/usr/bin/env python3 import horus def controller_tick(node): temp = node.recv("temperature") if temp is not None: if temp > 25.0: fan_speed = min(100, int((temp - 20) * 10)) node.send("fan_control", fan_speed) print(f"Controller: Fan at {fan_speed}%") else: node.send("fan_control", 0) print(f"Controller: Temperature normal, fan off") controller = horus.Node(name="ControllerNode", tick=controller_tick, order=1, rate=2, subs=["temperature"], pubs=["fan_control"]) horus.run(controller) ``` ### Logger Node **nodes/logger.py:** ```python #!/usr/bin/env python3 import horus import datetime def logger_tick(node): temp = node.recv("temperature") fan = node.recv("fan_control") if temp is not None and fan is not None: timestamp = datetime.datetime.now().strftime("%H:%M:%S") status = "COOLING" if fan > 0 else "NORMAL" print(f"Logger [{timestamp}]: {temp:.1f}°C | Fan {fan}% | {status}") logger = horus.Node(name="LoggerNode", tick=logger_tick, order=2, rate=1, subs=["temperature", "fan_control"]) horus.run(logger) ``` ### Run All Nodes Concurrently ```bash # Make scripts executable chmod +x nodes/*.py # Run all nodes as separate processes horus run "nodes/*.py" ``` **Output:** ```bash Executing 3 files concurrently: 1. nodes/controller.py (python) 2. nodes/logger.py (python) 3. nodes/sensor.py (python) Phase 1: Building all files... Phase 2: Starting all processes... Started [controller] Started [logger] Started [sensor] All processes running. Press Ctrl+C to stop. [sensor] Sensor: 23.4°C [controller] Controller: Fan at 34% [logger] Logger [15:30:45]: 23.4°C | Fan 34% | COOLING [sensor] Sensor: 26.8°C [controller] Controller: Fan at 68% [sensor] Sensor: 21.2°C [logger] Logger [15:30:46]: 21.2°C | Fan 12% | COOLING ``` **Key Features**: - **Independent Processes**: Each node runs in its own process - **Shared Memory IPC**: Nodes communicate via HORUS shared memory topics - **Color-Coded Output**: Each node has a unique color - **Graceful Shutdown**: Ctrl+C stops all processes cleanly - **Zero Configuration**: No launch files needed --- ## 4. Rust + Python Cross-Language System Mix Rust and Python nodes in the same application. ### Rust Sensor Node **nodes/rust_sensor.rs:** ```rust use horus::prelude::*; pub struct TempSensor { temp_pub: Topic, counter: f32, } impl TempSensor { fn new() -> Result { Ok(Self { temp_pub: Topic::new("temperature")?, counter: 0.0, }) } } impl Node for TempSensor { fn name(&self) -> &str { "RustTempSensor" } fn init(&mut self) -> Result<()> { hlog!(info, "Rust sensor online - high performance mode"); Ok(()) } fn tick(&mut self) { // Fast sensor simulation let temp = 20.0 + (self.counter.sin() * 5.0); self.temp_pub.send(temp); hlog!(debug, "Rust: {:.2}°C", temp); self.counter += 0.1; } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Rust sensor offline"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(TempSensor::new()?).order(0).build()?; scheduler.run() } ``` ### Python Controller Node **nodes/py_controller.py:** ```python #!/usr/bin/env python3 import horus def py_controller_tick(node): temp = node.recv("temperature") if temp is not None: status = "HOT" if temp > 22.0 else "NORMAL" print(f"Python controller: {temp:.2f}°C - {status}") command = 1.0 if temp > 22.0 else 0.0 node.send("actuator_cmd", command) controller = horus.Node(name="PyController", tick=py_controller_tick, order=0, rate=10, subs=["temperature"], pubs=["actuator_cmd"]) horus.run(controller) ``` ### Rust Actuator Node **nodes/rust_actuator.rs:** ```rust use horus::prelude::*; struct Actuator { cmd_sub: Topic, } impl Node for Actuator { fn name(&self) -> &str { "RustActuator" } fn tick(&mut self) { if let Some(cmd) = self.cmd_sub.recv() { let action = if cmd > 0.5 { "COOLING" } else { "IDLE" }; hlog!(info, "Actuator: {}", action); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Actuator stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(Actuator { cmd_sub: Topic::new("actuator_cmd")?, }).order(0).build()?; scheduler.run() } ``` ### Run Mixed System ```bash horus run "nodes/*" ``` HORUS automatically detects file types and compiles/runs appropriately! **Key Concepts**: - Rust nodes: High performance, type safety - Python nodes: Rapid prototyping, easy scripting - Shared memory IPC works across languages - Zero-copy message passing - Sub-microsecond latency even across language boundaries --- ## 5. Advanced Python Features Per-node rates, timestamp checking, and staleness detection. **File: `advanced_python.py`** ```python #!/usr/bin/env python3 import horus from horus import Imu # 100Hz IMU sensor def imu_tick(node): imu = Imu( accel_x=1.0, accel_y=0.0, accel_z=9.8, gyro_x=0.0, gyro_y=0.0, gyro_z=0.0 ) node.send("imu", imu) # 50Hz controller def controller_tick(node): imu = node.recv("imu") if imu is not None: # Process IMU data accel_magnitude = ( imu.accel_x**2 + imu.accel_y**2 + imu.accel_z**2 ) ** 0.5 print(f"Control: IMU magnitude = {accel_magnitude:.2f} m/s²") # Send command cmd = {"linear": 1.0, "angular": 0.5} node.send("cmd_vel", cmd) # 10Hz logger def logger_tick(node): msg = node.recv("cmd_vel") if msg is not None: print(f"Logger: Received command: {msg}") # Create nodes with per-node rates and run sensor = horus.Node(name="HighFreqSensor", tick=imu_tick, order=0, rate=100, pubs=["imu"]) ctrl = horus.Node(name="Controller", tick=controller_tick, order=1, rate=50, subs=["imu"], pubs=["cmd_vel"]) logger = horus.Node(name="Logger", tick=logger_tick, order=2, rate=10, subs=["cmd_vel"]) horus.run(sensor, ctrl, logger) ``` **Run it**: ```bash horus run advanced_python.py ``` **Key Concepts**: - **Per-node rates**: Each node runs at different frequency via `.rate(Hz)` - **Typed messages**: Use `Topic(Imu)` for cross-language compatible messages - **Generic messages**: Use `Topic("name")` for Python-only dict/list data - **Priorities**: Lower order number = higher priority, runs first each tick --- ## When to Use Multi-Process vs Single-Process ### Multi-Process (Concurrent Execution) **Use when:** - Nodes need to run independently (like ROS) - Want fault isolation (one crash doesn't kill others) - Different nodes have vastly different rates - Testing distributed system architectures - Mixing languages (Rust + Python) **Command:** ```bash horus run "nodes/*.rs" horus run "nodes/*.py" horus run "nodes/*" # Mix Rust and Python! ``` ### Single-Process **Use when:** - All nodes in one file - Need maximum performance - Require deterministic execution order - Simpler deployment - Embedded systems with limited resources **Command:** ```bash horus run main.rs ``` --- ## Performance Notes ### Multi-Process IPC Performance - **Latency**: 300ns - 1μs (shared memory) - **Throughput**: Thousands of messages/second - **Scalability**: Tested with 50+ concurrent processes - **Memory**: ~2-5MB overhead per process ### Single-Process Performance - **Latency**: 50-200ns (in-memory) - **Throughput**: Millions of messages/second - **Scalability**: Hundreds of nodes in one process - **Memory**: Minimal overhead --- ## Testing Multi-Node Systems **Create test script: `test_system.py`** ```python #!/usr/bin/env python3 import horus # Shared test state fan_commands = [] def mock_sensor_tick(node): node.send("temperature", 30.0) # Hot! def test_controller_tick(node): temp = node.recv("temperature") if temp is not None and temp > 25.0: fan_speed = min(100, int((temp - 20) * 10)) fan_commands.append(fan_speed) node.send("fan_control", fan_speed) sensor = horus.Node(name="MockSensor", tick=mock_sensor_tick, pubs=["temperature"]) controller = horus.Node(name="TestController", tick=test_controller_tick, subs=["temperature"], pubs=["fan_control"]) # Use tick_once for testing (runs one tick cycle) scheduler = horus.Scheduler() scheduler.add(sensor) scheduler.add(controller) scheduler.tick_once() # Verify assert len(fan_commands) > 0, "Controller should have sent fan commands" assert fan_commands[0] == 100, f"Expected fan at 100%, got {fan_commands[0]}%" print("Test passed!") ``` --- ## Next Steps - [Performance Optimization](/performance/performance) - Tune for maximum throughput - [Python Bindings Reference](/python/api/python-bindings) - Complete Python API - [Package Management](/package-management/package-management) - Share and reuse nodes Need help? - [Troubleshooting](/troubleshooting) - Debug common issues - [Monitor](/development/monitor) - Monitor your system in real-time --- ## horus_macros Path: /rust/api/macros Description: Procedural macros for reducing boilerplate # horus_macros Macros that eliminate boilerplate. Pick the right one: | I need to... | Use | Example | |---|---|---| | Define a node with pub/sub | `node!` | Sensor reader, motor controller | | Define a custom message type | `message!` | `MotorFeedback`, `WheelOdometry` | | Define a request/response service | `service!` | `CalibrateImu`, `GetMapRegion` | | Define a long-running task | `action!` | `NavigateTo`, `PickAndPlace` | | Use a standard action template | `standard_action!` | `navigate`, `manipulate`, `dock` | | Log from inside a node | `hlog!` | `hlog!(info, "Motor started")` | ```rust use horus::prelude::*; // Includes all macros ``` --- ## node! Declarative macro for creating HORUS nodes with minimal boilerplate. ### Syntax ```rust node! { NodeName { name: "custom_name", // Custom node name (optional) rate 100.0 // Tick rate in Hz (optional) pub { ... } // Publishers (optional) sub { ... } // Subscribers (optional) data { ... } // Internal state (optional) tick { ... } // Main loop (required) init { ... } // Initialization (optional) shutdown { ... } // Cleanup (optional) impl { ... } // Custom methods (optional) } } ``` Only the node name and `tick` are required. Everything else is optional. ### Sections #### `pub` - Publishers Define topics this node publishes to. ```rust pub { // Syntax: name: Type -> "topic_name" velocity: f32 -> "robot.velocity", status: String -> "robot.status", pose: Pose2D -> "robot.pose" } ``` **Generated code:** - `Topic` field for each publisher - Automatic initialization in `new()` #### `sub` - Subscribers Define topics this node subscribes to. ```rust sub { // Syntax: name: Type -> "topic_name" commands: String -> "user.commands", sensors: f32 -> "sensors.temperature" } ``` **Generated code:** - `Topic` field for each subscriber - Automatic initialization in `new()` #### `data` - Internal State Define internal fields with default values. ```rust data { counter: u32 = 0, buffer: Vec = Vec::new(), last_time: Instant = Instant::now(), config: MyConfig = MyConfig::default() } ``` #### `tick` - Main Loop **Required.** Called every scheduler cycle (~100 Hz by default). ```rust tick { // Read from subscribers if let Some(cmd) = self.commands.recv() { // Process } // Write to publishers self.velocity.send(1.0); // Access internal state self.counter += 1; } ``` #### `init` - Initialization Called once before the first tick. The block must return `Ok(())` on success (it generates `fn init(&mut self) -> Result<()>`). ```rust init { hlog!(info, "Node starting"); self.buffer.reserve(1000); Ok(()) } ``` #### `shutdown` - Cleanup Called once when the scheduler stops. Must return `Ok(())` on success (generates `fn shutdown(&mut self) -> Result<()>`). ```rust shutdown { hlog!(info, "Node stopping"); // Close files, save state, etc. Ok(()) } ``` #### `impl` - Custom Methods Add helper methods to the node. ```rust impl { fn calculate(&self, x: f32) -> f32 { x * 2.0 + self.offset } fn reset(&mut self) { self.counter = 0; } } ``` ### Generated Code The macro generates: 1. **`pub struct NodeName`** with `Topic` fields for publishers/subscribers and your data fields 2. **`impl NodeName { pub fn new() -> Self }`** constructor that creates all Topics 3. **`impl Node for NodeName`** with `name()`, `tick()`, optional `init()`, `shutdown()`, `publishers()`, `subscribers()`, and `rate()` 4. **`impl Default for NodeName`** that calls `Self::new()` 5. **`impl NodeName { ... }`** for any methods from the `impl` section ```rust // This macro call: node! { SensorNode { pub { data: f32 -> "sensor" } data { count: u32 = 0 } tick { self.count += 1; } } } // Generates approximately: pub struct SensorNode { data: Topic, count: u32, } impl SensorNode { pub fn new() -> Self { Self { data: Topic::new("sensor").expect("Failed to create publisher 'sensor'"), count: 0, } } } impl Node for SensorNode { fn name(&self) -> &str { "sensor_node" } // Auto snake_case fn tick(&mut self) { self.count += 1; } } impl Default for SensorNode { fn default() -> Self { Self::new() } } ``` The struct name is converted to snake_case for the node name (e.g., `SensorNode` becomes `"sensor_node"`), unless overridden with `name:`. ### Examples #### Minimal Node ```rust node! { MinimalNode { tick { // Called every tick } } } ``` #### Publisher Only ```rust node! { HeartbeatNode { pub { alive: bool -> "system.heartbeat" } data { count: u64 = 0 } tick { self.alive.send(true); self.count += 1; } } } ``` #### Subscriber Only ```rust node! { LoggerNode { sub { messages: String -> "logs" } tick { while let Some(msg) = self.messages.recv() { hlog!(info, "{}", msg); } } } } ``` #### Full Pipeline ```rust node! { ProcessorNode { sub { input: f32 -> "raw_data" } pub { output: f32 -> "processed_data" } data { scale: f32 = 2.0, offset: f32 = 10.0 } tick { if let Some(value) = self.input.recv() { let result = value * self.scale + self.offset; self.output.send(result); } } impl { fn set_scale(&mut self, scale: f32) { self.scale = scale; } } } } ``` #### With Lifecycle ```rust node! { StatefulNode { pub { status: String -> "status" } data { initialized: bool = false, tick_count: u64 = 0 } init { hlog!(info, "Initializing..."); self.initialized = true; Ok(()) } tick { self.tick_count += 1; let msg = format!("Tick {}", self.tick_count); self.status.send(msg); } shutdown { hlog!(info, "Total ticks: {}", self.tick_count); Ok(()) } } } ``` ### Usage ```rust use horus::prelude::*; node! { MyNode { pub { output: f32 -> "data" } tick { self.output.send(42.0); } } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(MyNode::new()).order(0).build()?; scheduler.run() } ``` --- ## `#[derive(LogSummary)]` Derive macro for implementing the `LogSummary` trait with default `Debug` formatting. ### When to Use Use `#[derive(LogSummary)]` when you need `Topic::verbose flag (via TUI monitor)` on a custom message type. `LogSummary` is **not** required for basic `Topic::new()` — only for the opt-in introspection mode. The derive requires `Debug` on the type since it generates a `Debug`-based implementation. ```rust use horus::prelude::*; #[derive(Debug, Clone, Serialize, Deserialize, LogSummary)] pub struct MyStatus { pub temperature: f32, pub voltage: f32, } // Now you can use verbose flag (via TUI monitor) let topic: Topic = Topic::new("status")?; ``` The derive generates: ```rust impl LogSummary for MyStatus { fn log_summary(&self) -> String { format!("{:?}", self) } } ``` ### Custom LogSummary For large types (images, point clouds) where `Debug` output would be too verbose, implement `LogSummary` manually instead of deriving: ```rust use horus::prelude::*; impl LogSummary for MyLargeData { fn log_summary(&self) -> String { format!("MyLargeData({}x{}, {} bytes)", self.width, self.height, self.data.len()) } } ``` --- ## Best Practices ### Keep tick Fast ```rust // Good - non-blocking tick { if let Some(x) = self.input.recv() { self.output.send(x * 2.0); } } // Bad - blocking operation tick { std::thread::sleep(Duration::from_secs(1)); // Blocks scheduler! } ``` ### Pre-allocate in init ```rust init { self.buffer.reserve(1000); // Do once Ok(()) } tick { // Don't allocate here - runs every tick } ``` ### Use Descriptive Names ```rust // Good pub { motor_velocity: f32 -> "motors.velocity" } // Bad pub { x: f32 -> "data" } ``` ### Handle Errors Gracefully ```rust tick { // send() is infallible — always succeeds self.status.send("ok".to_string()); // No error handling needed — ring buffer overwrites oldest on full self.critical.send(data); } ``` --- ## Troubleshooting ### "Cannot find type in scope" Import message types: ```rust use horus::prelude::*; node! { MyNode { pub { cmd: CmdVel -> "cmd_vel" } tick { } } } ``` ### "Expected `,`, found `{`" Check arrow syntax: ```rust // Wrong pub { cmd: f32 "topic" } // Correct pub { cmd: f32 -> "topic" } ``` ### "Node name must be CamelCase" ```rust // Wrong node! { my_node { ... } } // Correct node! { MyNode { ... } } ``` ### Use hlog! for logging ```rust tick { // Use hlog! macro for logging hlog!(info, "test"); hlog!(debug, "value = {}", some_value); hlog!(warn, "potential issue"); hlog!(error, "something went wrong"); } ``` --- ## Logging Macros ### hlog! Node-aware logging that publishes to the shared memory log buffer (visible in monitor) and emits to stderr with ANSI colors. ```rust hlog!(info, "Sensor initialized"); hlog!(debug, "Value: {}", some_value); hlog!(warn, "Battery low: {}%", battery_pct); hlog!(error, "Failed to read sensor: {}", err); ``` Levels: `trace`, `debug`, `info`, `warn`, `error` The scheduler automatically sets the current node context, so log messages include the node name: ``` [INFO] [SensorNode] Sensor initialized ``` ### hlog_once! Log a message **once per callsite**. Subsequent calls from the same source location are silently ignored. Uses a per-callsite `AtomicBool` — zero overhead after the first call. ```rust fn tick(&mut self) { // Log when sensor first produces valid data hlog_once!(info, "Sensor online — first reading: {:.2}", value); // Warn about a condition the first time it's detected if self.error_count > 0 { hlog_once!(warn, "Sensor errors detected — check wiring"); } } ``` Common uses: first-connection notifications, one-time calibration messages, feature availability checks at startup. ### hlog_every! Throttled logging — emits at most once per `interval_ms` milliseconds. Uses a per-callsite `AtomicU64` timestamp — zero overhead when the interval hasn't elapsed. Essential for nodes running at high frequencies (100Hz+) where per-tick logging would flood the system. ```rust fn tick(&mut self) { // Status heartbeat every 5 seconds hlog_every!(5000, info, "Motor controller OK — speed: {:.1} rad/s", self.velocity); // Battery warnings every second (not every tick at 1kHz) if self.battery_pct < 20.0 { hlog_every!(1000, warn, "Battery low: {:.0}%", self.battery_pct); } // Periodic performance stats every 10 seconds hlog_every!(10_000, debug, "Avg latency: {:.1}us, ticks: {}", self.avg_latency_us, self.tick_count); } ``` --- ## message! Declarative macro for defining custom message types for use with `Topic`. Auto-derives all required traits so messages work with zero configuration. ### Syntax ```rust use horus::prelude::*; message! { /// Motor command sent to actuators MotorCommand { velocity: f32, torque: f32, } } // Ready to use with Topic let topic: Topic = Topic::new("motor.cmd")?; topic.send(MotorCommand { velocity: 1.0, torque: 0.5 }); ``` ### Multiple Messages Define multiple message types in a single block: ```rust message! { /// Velocity command CmdVel { linear_x: f64, angular_z: f64, } /// Battery status BatteryStatus { voltage: f32, current: f32, percentage: f32, } } ``` ### Generated Code For `message! { Foo { x: f32, y: f32 } }`, the macro generates: ```rust,ignore #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Foo { pub x: f32, pub y: f32, } impl LogSummary for Foo { fn log_summary(&self) -> String { format!("{:?}", self) } } ``` The struct automatically integrates with HORUS topics for zero-copy transport — no additional trait implementation is needed. ### When to Use Use `message!` when HORUS's [standard message types](/rust/api/messages) don't cover your domain. For standard robotics types (Twist, Pose2D, Imu, etc.), use the prelude types instead. --- ### action! and service! Macros See [Actions](/concepts/actions) and [Services](/concepts/services) for the `action!` and `service!` macros that generate typed communication patterns. --- ## topics! Define compile-time topic descriptors for type-safe, typo-proof topic names across your codebase. ### Syntax ```rust use horus::prelude::*; topics! { pub CMD_VEL: CmdVel = "cmd_vel", pub SENSOR_DATA: Imu = "sensor.imu", pub MOTOR_STATUS: MotorCommand = "motor.cmd", } ``` Each entry creates a `TopicDescriptor` constant. Use it to create topics with guaranteed name and type consistency: ### Usage ```rust // Instead of string literals (typo-prone): let topic: Topic = Topic::new("cmd_vel")?; // Use typed descriptors (compile-time checked): let topic = CMD_VEL.create()?; // Topic, name = "cmd_vel" ``` ### Benefits - **No typos** — topic name is defined once, referenced everywhere - **Type safety** — `SENSOR_DATA.create()` always returns `Topic`, never `Topic` - **Discoverability** — grep for `CMD_VEL` to find all publishers and subscribers --- ## See Also - [node! Macro Guide](/concepts/node-macro) - Detailed tutorial - [API Reference](/rust/api) - Core types reference - [Actions](/concepts/actions) - action! macro reference - [Services](/concepts/services) - service! macro reference --- ## Scheduler API Path: /rust/api/scheduler Description: Complete API reference for the HORUS Scheduler and node configuration # Scheduler API The Scheduler orchestrates node execution with composable builder configuration. ```rust use horus::prelude::*; ``` ## Scheduler ### Constructor ```rust let mut scheduler = Scheduler::new(); ``` Creates a scheduler with auto-detected platform capabilities (RT support, CPU topology, memory locking). ### Builder Methods All builder methods return `Self` for chaining: ```rust let mut scheduler = Scheduler::new() .tick_rate(1000_u64.hz()) .prefer_rt() .watchdog(500_u64.ms()) .blackbox(64) .max_deadline_misses(3) .verbose(false) .with_recording(); ``` ```rust // Named scheduler (useful for multi-scheduler setups) let mut scheduler = Scheduler::new() .name("motor_control") .tick_rate(1000_u64.hz()); ``` | Method | Default | Description | |--------|---------|-------------| | `.name(name)` | `"Scheduler"` | Set the scheduler name | | `.tick_rate(freq)` | 100 Hz | Global scheduler tick rate | | `.prefer_rt()` | — | Try RT features (mlockall, SCHED_FIFO), warn on failure | | `.require_rt()` | — | Enable RT features, **panic** if unavailable | | `.deterministic(bool)` | `false` | Sequential single-threaded execution (no parallelism) | | `.watchdog(Duration)` | disabled | Detect frozen nodes (auto-creates safety monitor) | | `.blackbox(size_mb)` | disabled | Flight recorder for crash forensics | | `.max_deadline_misses(n)` | 100 | Emergency stop threshold | | `.cores(&[usize])` | all cores | Pin scheduler threads to specific CPU cores | | `.verbose(bool)` | true | Enable/disable non-emergency logging | | `.with_recording()` | disabled | Enable session record/replay | | `.telemetry(endpoint)` | disabled | Export metrics to HTTP endpoint | ### Detailed Method Reference --- #### new ```rust pub fn new() -> Self ``` Creates a scheduler with auto-detected platform capabilities. **What happens on construction:** 1. Detects RT capabilities: `SCHED_FIFO` support, `mlockall` permission, CPU topology (~30-100us) 2. Cleans up stale SHM namespaces from previously crashed processes (< 1ms) 3. Sets default tick rate to 100 Hz **Returns:** `Scheduler` with default configuration. No nodes registered. **Example:** ```rust let mut scheduler = Scheduler::new(); // Scheduler is ready — add nodes and call .run() ``` --- #### tick_rate ```rust pub fn tick_rate(self, freq: Frequency) -> Self ``` Sets the global scheduler loop frequency. **Parameters:** - `freq: Frequency` — Target tick rate. Create with `.hz()` extension: `100_u64.hz()`, `1000_u64.hz()`. The `Frequency` type validates at construction — panics on 0, NaN, infinite, or negative values. **Returns:** `Self` (chainable). **Panics:** Indirectly — the `Frequency` constructor panics on invalid values: ```rust 0_u64.hz(); // PANICS: zero frequency ``` **Relationship to node rates:** Node `.rate(freq)` can differ from the global tick rate. The scheduler runs at its rate; nodes with different rates are tick-divided. **Examples:** ```rust // 100 Hz default control loop let s = Scheduler::new().tick_rate(100.hz()); // 1 kHz high-frequency servo control let s = Scheduler::new().tick_rate(1000.hz()); ``` --- #### prefer_rt ```rust pub fn prefer_rt(self) -> Self ``` Try to enable OS-level RT features. Degrades gracefully if unavailable. **What it enables:** `mlockall` (prevent page faults) + `SCHED_FIFO` (real-time scheduling class). **Behavior:** If the system lacks RT capabilities (no `SCHED_FIFO` or `mlockall`), logs a warning and continues without them. Your code still runs — just without RT guarantees. **When to use:** Production robots where RT is desired but the system might not support it (e.g., development on a non-RT kernel). **Example:** ```rust let s = Scheduler::new().prefer_rt(); // On RT kernel: mlockall + SCHED_FIFO enabled // On non-RT kernel: warning logged, runs normally ``` --- #### require_rt ```rust pub fn require_rt(self) -> Self ``` Enable OS-level RT features. **Panics** if unavailable. **Panics:** If the system has neither `SCHED_FIFO` nor `mlockall` support. **When to use:** Safety-critical deployments where running without RT is unacceptable. Forces the developer to fix the deployment environment rather than silently degrading. **Example:** ```rust let s = Scheduler::new().require_rt(); // On non-RT kernel: PANICS with "RT capabilities required but not available" ``` --- #### deterministic ```rust pub fn deterministic(self, enabled: bool) -> Self ``` Enable deterministic execution mode. **Parameters:** - `enabled: bool` — `true` to enable, `false` to disable (default). **What changes:** - Clock switches from `WallClock` to `SimClock` (virtual time, advances by exact `dt` per tick) - Execution is sequential (no parallelism) to ensure deterministic ordering - Two runs with same inputs produce identical results regardless of CPU speed **When to use:** Simulation, reproducible testing, CI pipelines. **Example:** ```rust let s = Scheduler::new() .tick_rate(100.hz()) .deterministic(true); // Time advances exactly 10ms per tick, regardless of actual CPU time ``` **See Also:** [Clock API](/rust/api/clock-api), [Deterministic Mode](/advanced/deterministic-mode) --- #### build (NodeBuilder) ```rust pub fn build(self) -> Result<&mut Scheduler> ``` Validate the node configuration and register it with the scheduler. **Returns:** `Result<&mut Scheduler>` — the scheduler for further `.add()` calls. **Errors (rejects):** - Budget or deadline set on non-RT execution class → `Error` - Zero budget or deadline → `Error` - Empty topic name in `.on("")` → `Error` - Budget > deadline → `Error` **Warnings (allows but warns):** - `.priority()` on non-RT node → logged warning - `.core()` on non-RT node → logged warning - `.on_miss()` on node without deadline → logged warning **What happens internally:** 1. `finalize()` runs RT auto-derivation: if `.rate()` was set on a BestEffort node, promotes to RT with 80% budget and 95% deadline 2. Validates all constraints 3. Registers node with the scheduler **Example:** ```rust // This succeeds: scheduler.add(my_node).rate(100.hz()).build()?; // This fails (budget > deadline): scheduler.add(my_node) .budget(10.ms()) .deadline(5.ms()) .build()?; // Err: budget exceeds deadline ``` --- #### run ```rust pub fn run(&mut self) -> Result<()> ``` Start the main scheduler loop. **Blocks** until `Ctrl+C` or `.stop()` is called. **Returns:** `Result<()>` — `Ok` on graceful shutdown, `Err` on fatal error. **Behavior:** 1. Installs `SIGINT`/`SIGTERM` signal handler 2. Calls `init()` on all nodes (lazy init on first run) 3. Loops: tick all nodes → sleep until next period → repeat 4. On shutdown: calls `shutdown()` on all nodes, prints timing report **Example:** ```rust scheduler.run()?; // Blocks until Ctrl+C ``` --- #### tick_once ```rust pub fn tick_once(&mut self) -> Result<()> ``` Execute exactly one tick cycle, then return. No loop, no sleep. **Behavior:** - Calls `init()` on all nodes if not yet initialized (lazy init) - Ticks every registered node once in order - Returns immediately after all nodes have ticked **When to use:** Testing, simulation stepping, integration tests. **Example:** ```rust let mut scheduler = Scheduler::new(); scheduler.add(my_node).build()?; // Step through 100 ticks manually for _ in 0..100 { scheduler.tick_once()?; } ``` --- ### Adding Nodes ```rust scheduler.add(my_node) .order(10) // NOTE: .rate() auto-enables RT scheduling with 80% budget and 95% deadline .rate(500_u64.hz()) .build()?; ``` `.add(node)` accepts any node — your own structs, driver handles, or factory results. Chain configuration, then `.build()` to register. **Node configuration chain:** | Method | Default | Description | |--------|---------|-------------| | `.order(u32)` | 100 | Execution priority (lower = earlier). 0-9: critical, 10-49: sensors, 50-99: processing, 100+: background | | `.rate(Frequency)` | global | Per-node tick rate. Auto-enables RT scheduling with 80% budget, 95% deadline | | `.budget(Duration)` | 80% of period | Max execution time per tick. If deadline is not set, deadline = budget (auto-derived) | | `.deadline(Duration)` | 95% of period | Hard deadline per tick. When exceeded, the `Miss` policy fires | | `.on_miss(Miss)` | `Warn` | What to do on deadline miss: `Warn`, `Skip`, `SafeMode`, `Stop` | | `.failure_policy(FailurePolicy)` | none | Recovery on tick failure: `Fatal`, `restart(n, backoff)`, `skip(n, cooldown)`, `Ignore` | | `.compute()` | — | Run on parallel thread pool (CPU-bound work) | | `.on(topic)` | — | Event-triggered: tick when topic receives data (empty topic rejected) | | `.async_io()` | — | Run on tokio blocking pool (I/O-bound work) | | `.priority(i32)` | none | OS `SCHED_FIFO` priority (1-99, RT only — warned if non-RT) | | `.core(usize)` | none | Pin to CPU core (RT only — warned if non-RT) | | `.watchdog(Duration)` | global | Per-node watchdog timeout override | | `.build()` | — | Validate configuration and register. Rejects conflicts, warns misuse | > **What `.build()` validates:** Rejects budget/deadline on non-RT classes, zero budget/deadline, empty `.on("")` topics. Warns (but allows) `.priority()`, `.core()`, and `.on_miss()` on nodes without RT/deadline. See [Validation & Conflicts](/concepts/execution-classes#validation--conflicts) for the full matrix. ### Execution Control | Method | Returns | Description | |--------|---------|-------------| | `.run()` | `Result<()>` | Main loop — runs until Ctrl+C or `.stop()` | | `.run_for(duration)` | `Result<()>` | Run for specific duration, then shut down | | `.tick_once()` | `Result<()>` | Execute exactly one tick cycle (no loop, no sleep) | | `.tick(&[names])` | `Result<()>` | One tick for specific nodes only | | `.set_node_rate(name, freq)` | `&mut Self` | Change a node's tick rate at runtime | | `.stop()` | `()` | Signal graceful shutdown | | `.is_running()` | `bool` | Check if scheduler is actively ticking | ### Monitoring | Method | Returns | Description | |--------|---------|-------------| | `.metrics()` | `Vec` | Performance metrics for all nodes | | `.node_list()` | `Vec` | Names of all registered nodes | | `.status()` | `String` | Formatted status report | ### Example ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(1000_u64.hz()) .prefer_rt() .watchdog(500_u64.ms()); // NOTE: .rate() auto-promotes this node to RT with budget=1.6ms (80%) and deadline=1.9ms (95%) scheduler.add(SensorNode::new()) .order(0) .rate(500_u64.hz()) .build()?; // NOTE: .compute() runs on a worker thread pool — no RT scheduling applied scheduler.add(PlannerNode::new()) .order(50) .compute() .build()?; scheduler.add(ControlNode::new()) .order(10) // NOTE: .rate() auto-promotes to RT; .on_miss(SafeMode) calls enter_safe_state() on deadline miss .rate(1000_u64.hz()) .on_miss(Miss::SafeMode) .build()?; scheduler.run()?; Ok(()) } ``` --- ## Node Configuration After `.add(node)`, chain these methods to configure the node before `.build()`. ### Timing & Ordering | Method | Description | |--------|-------------| | `.order(n)` | Execution priority within a tick (lower runs first) | | `.rate(Frequency)` | Node-specific tick rate — auto-derives budget (80%) and deadline (95%), auto-marks as RT | | `.budget(Duration)` | Explicit CPU time budget per tick | | `.deadline(Duration)` | Explicit deadline per tick | | `.on_miss(Miss)` | Deadline miss policy: `Warn`, `Skip`, `SafeMode`, `Stop` | ### Execution Class | Method | Class | Description | |--------|-------|-------------| | `.compute()` | Compute | Offloaded to worker thread pool (planning, SLAM, ML) | | `.on(topic)` | Event | Wakes only when the named topic has new data | | `.async_io()` | AsyncIo | Runs on async executor (network, disk, cloud) | | *(none)* | BestEffort | Default — ticks every scheduler cycle | | `.rate(freq)` | RT (auto) | Auto-promoted when rate is set on a BestEffort node | ### Failure & Finalization | Method | Description | |--------|-------------| | `.failure_policy(policy)` | Per-node failure handling (`Fatal`, `Restart`, `Skip`, `Ignore`) | | `.build()` | Finalize and register — returns `Result<&mut Scheduler>` | ### Order Guidelines | Range | Use | |-------|-----| | 0-9 | Critical real-time (motor control, safety) | | 10-49 | High priority (sensors, fast loops) | | 50-99 | Normal (processing, planning) | | 100-199 | Low (logging, diagnostics) | | 200+ | Background (telemetry) | ### Node Builder Detailed Reference --- #### rate (NodeBuilder) ```rust pub fn rate(self, freq: Frequency) -> Self ``` Set a per-node tick rate. **Automatically promotes the node to RT execution class** with derived budget (80% of period) and deadline (95% of period). **Parameters:** - `freq: Frequency` — Node-specific tick rate. Created with `.hz()`: `500_u64.hz()`. **Auto-derivation:** When `.rate()` is called on a BestEffort node: - `budget = period * 0.80` (e.g., 500 Hz → period 2ms → budget 1.6ms) - `deadline = period * 0.95` (e.g., 500 Hz → period 2ms → deadline 1.9ms) - Execution class → `Rt` **Interaction with `.budget()` and `.deadline()`:** Explicit `.budget()` or `.deadline()` override the auto-derived values. Order doesn't matter — derivation happens at `finalize()`. **Example:** ```rust scheduler.add(sensor) .rate(500.hz()) // RT: budget=1.6ms, deadline=1.9ms .build()?; scheduler.add(motor) .rate(1000.hz()) .budget(500.us()) // Override: budget=0.5ms instead of 0.8ms .build()?; ``` --- #### budget (NodeBuilder) ```rust pub fn budget(self, duration: Duration) -> Self ``` Set the maximum CPU time allowed per tick. **Parameters:** - `duration: Duration` — Maximum execution time. Created with `.us()` or `.ms()`: `200_u64.us()`, `1_u64.ms()`. **Errors:** `.build()` rejects if: - `budget` is zero - `budget > deadline` - `budget` is set on a non-RT node (no `.rate()` or explicit RT class) **Example:** ```rust scheduler.add(planner) .rate(100.hz()) .budget(5.ms()) // Must complete tick within 5ms .deadline(8.ms()) // Hard deadline at 8ms .on_miss(Miss::Skip) .build()?; ``` --- #### on_miss (NodeBuilder) ```rust pub fn on_miss(self, policy: Miss) -> Self ``` Set the policy for when a node exceeds its deadline. **Parameters:** - `policy: Miss` — One of `Warn`, `Skip`, `SafeMode`, `Stop`. **Default:** `Miss::Warn`. ### Miss Enum | Variant | Behavior | Use Case | |---------|----------|----------| | `Miss::Warn` | Log warning, continue | Soft real-time (logging, UI) | | `Miss::Skip` | Skip this tick | Firm real-time (video encoding) | | `Miss::SafeMode` | Call `enter_safe_state()` | Motor controllers, safety nodes | | `Miss::Stop` | Stop entire scheduler | Hard real-time safety-critical | Default is `Miss::Warn`. --- ## NodeMetrics Returned by `scheduler.metrics()`. Read-only performance data for each node. | Method | Returns | Description | |--------|---------|-------------| | `.name()` | `&str` | Node name | | `.order()` | `u32` | Execution order | | `.total_ticks()` | `u64` | Total tick count | | `.successful_ticks()` | `u64` | Ticks without errors | | `.avg_tick_duration_ms()` | `f64` | Mean tick duration | | `.max_tick_duration_ms()` | `f64` | Worst-case tick duration | | `.min_tick_duration_ms()` | `f64` | Best-case tick duration | | `.last_tick_duration_ms()` | `f64` | Most recent tick duration | | `.messages_sent()` | `u64` | Total messages published | | `.messages_received()` | `u64` | Total messages consumed | | `.errors_count()` | `u64` | Error count | | `.warnings_count()` | `u64` | Warning count | | `.uptime_seconds()` | `f64` | Node uptime | ### Performance Monitoring Example ```rust // After running for a while, check node health for metric in scheduler.metrics() { if metric.max_tick_duration_ms() > 1.0 { hlog!(warn, "Node '{}' worst case: {:.2}ms (avg: {:.2}ms)", metric.name(), metric.max_tick_duration_ms(), metric.avg_tick_duration_ms(), ); } } ``` --- ## RtStats Real-time execution statistics for nodes with `.rate()` set. Access via node-level timing reports. | Method | Returns | Description | |--------|---------|-------------| | `.deadline_misses()` | `u64` | Total deadline misses | | `.budget_violations()` | `u64` | Total budget violations | | `.worst_execution()` | `Duration` | Worst-case tick duration | | `.last_execution()` | `Duration` | Most recent tick duration | | `.jitter_us()` | `f64` | Execution jitter in microseconds | | `.avg_execution_us()` | `f64` | Average tick duration in microseconds | | `.sampled_ticks()` | `u64` | Number of ticks sampled | | `.summary()` | `String` | Formatted timing summary | ### Example ```rust use horus::prelude::*; let mut scheduler = Scheduler::new() .tick_rate(1000_u64.hz()) .prefer_rt(); scheduler.add(MotorController::new()) .order(0) // NOTE: .rate(1000.hz()) auto-derives budget=0.8ms and deadline=0.95ms .rate(1000_u64.hz()) // NOTE: Miss::SafeMode calls enter_safe_state() on the node when a deadline is missed .on_miss(Miss::SafeMode) .build()?; // Run for 5 seconds, then check stats scheduler.run_for(5_u64.secs())?; for metric in scheduler.metrics() { println!("{}: avg={:.2}ms, worst={:.2}ms, misses={}", metric.name(), metric.avg_tick_duration_ms(), metric.max_tick_duration_ms(), metric.errors_count(), ); } ``` --- ## Advanced Runtime Methods These methods require elevated privileges (root or `CAP_SYS_NICE`) on Linux. They fail gracefully with an error on unsupported platforms. | Method | Description | |--------|-------------| | `set_os_priority(i32)` | Set OS-level SCHED_FIFO priority (1-99, higher = more priority) | | `pin_to_cpu(usize)` | Pin scheduler thread to a specific CPU core | | `lock_memory()` | Lock all memory pages to prevent swapping (`mlockall`) | | `prefault_stack(usize)` | Pre-allocate stack pages to avoid page faults during execution | ```rust // Production RT setup — call before run() scheduler.set_os_priority(80)?; // High SCHED_FIFO priority scheduler.pin_to_cpu(2)?; // Isolated CPU core scheduler.lock_memory()?; // Prevent page faults scheduler.prefault_stack(512 * 1024)?; // 512KB stack scheduler.run()?; ``` These are automatically applied when using `.require_rt()` on the builder. Use these methods when you need manual control over which features to enable. ## Monitoring & Metrics | Method | Returns | Description | |--------|---------|-------------| | `rt_stats(node_name)` | `Option<&RtStats>` | RT timing stats for a specific node (deadline misses, worst execution, jitter) | | `safety_stats()` | `Option` | Aggregate safety stats (budget overruns, watchdog expirations) | | `metrics()` | `Vec` | Per-node metrics (tick counts, durations, errors) | | `node_list()` | `Vec` | Names of all registered nodes | | `running_flag()` | `Arc` | Shared flag for external stop control | ```rust // Check RT performance after a test run if let Some(stats) = scheduler.rt_stats("motor_ctrl") { println!("Deadline misses: {}", stats.deadline_misses()); println!("Worst execution: {:?}", stats.worst_execution()); println!("Jitter: {:.1} μs", stats.jitter_us()); } ``` --- ## Production Patterns ### Warehouse AGV Mobile robot with safety monitor, lidar SLAM, path planner, and motor control: ```rust let mut sched = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(100.hz()); // Safety runs first every tick — never skip sched.add(EmergencyStopMonitor::new()?).order(0).rate(100.hz()).on_miss(Miss::Stop).build()?; // Sensors at 50Hz sched.add(LidarDriver::new()?).order(10).rate(50.hz()).build()?; sched.add(WheelOdometry::new()?).order(11).rate(100.hz()).build()?; // SLAM is CPU-heavy — runs on thread pool sched.add(SlamNode::new()?).order(20).compute().build()?; // Planner at 10Hz — doesn't need to be fast sched.add(PathPlanner::new()?).order(30).rate(10.hz()).build()?; // Motor control at 100Hz with strict deadline sched.add(MotorController::new()?).order(40).rate(100.hz()).budget(5.ms()).on_miss(Miss::Skip).build()?; // Logger at 1Hz — background priority sched.add(TelemetryLogger::new()?).order(200).rate(1.hz()).build()?; sched.run()?; ``` ### Drone Flight Controller High-frequency IMU processing with tight deadlines: ```rust let mut sched = Scheduler::new() .require_rt() .watchdog(100_u64.ms()) .tick_rate(1000.hz()); sched.add(ImuReader::new()?).order(0).rate(1000.hz()).budget(200.us()).build()?; sched.add(AttitudeController::new()?).order(1).rate(1000.hz()).budget(300.us()).on_miss(Miss::SafeMode).build()?; sched.add(PositionController::new()?).order(2).rate(200.hz()).budget(1.ms()).build()?; sched.add(MotorMixer::new()?).order(3).rate(1000.hz()).budget(100.us()).build()?; sched.run()?; ``` --- ## See Also - [Scheduler Concepts](/concepts/core-concepts-scheduler) — Conceptual overview and architecture - [Scheduler Configuration](/advanced/scheduler-configuration) — Advanced tuning and deployment patterns - [Safety Monitor](/advanced/safety-monitor) — Budget enforcement and graduated degradation - [BlackBox](/advanced/blackbox) — Flight recorder for post-mortem analysis - [Node API](/concepts/core-concepts-nodes) — The Node trait and lifecycle - [HealthStatus](/rust/api/health-status) — Node health states (Healthy, Warning, Unhealthy, Isolated) - [NodeState](/rust/api/node-state) — Node lifecycle states and transitions - [Python Bindings](/python/api/python-bindings) — Python Scheduler API - [Driver API](/rust/api/drivers) — Load hardware from config --- ## Driver API Path: /rust/api/drivers Description: Load hardware connections from config, typed Terra handles, local driver registration # Driver API Load pre-configured hardware connections from `horus.toml` and use them in your nodes. ```rust use horus::prelude::*; use horus::drivers; ``` ## Two Paths **Config + Code** — config declares hardware, code controls scheduling: ```rust // horus.toml provides port, baudrate, servo_ids let mut hw = drivers::load()?; let handle = hw.dynamixel("arm")?; scheduler.add(ArmDriver::new(handle)) .rate(500_u64.hz()) .on_miss(Miss::SafeMode) .build()?; ``` **Pure Code** — no config, direct Terra usage: ```rust use terra::dynamixel::DynamixelBus; let bus = DynamixelBus::open("/dev/ttyUSB0", 1_000_000)?; scheduler.add(ArmDriver::new(bus)) .rate(500_u64.hz()) .build()?; ``` Both produce the same node with the same scheduling API. ## Loading Drivers ### `drivers::load()` Reads `horus.toml` `[drivers]` section from the current directory (searches up to 10 parents). ```rust let mut hw = drivers::load()?; ``` ### `drivers::load_from(path)` Load from a specific config file. Useful for testing or multi-robot setups. ```rust let mut hw = drivers::load_from("tests/test_drivers.toml")?; ``` ## `HardwareSet` Returned by `drivers::load()`. Provides typed accessors for each driver category. ### Terra Accessors Each returns a `DriverHandle` with the connection params from config: | Method | Config key | Hardware | |--------|-----------|----------| | `hw.dynamixel("name")` | `terra = "dynamixel"` | Dynamixel servo bus | | `hw.rplidar("name")` | `terra = "rplidar"` | RPLiDAR scanner | | `hw.realsense("name")` | `terra = "realsense"` | Intel RealSense camera | | `hw.i2c("name")` | `terra = "mpu6050"` etc. | I2C device | | `hw.serial("name")` | `terra = "vesc"` etc. | Serial/UART port | | `hw.can("name")` | `terra = "odrive"` etc. | CAN bus | | `hw.gpio("name")` | `terra = "gpio"` | GPIO pin | | `hw.pwm("name")` | `terra = "pwm"` | PWM output | | `hw.usb("name")` | `terra = "usb"` | USB device | | `hw.webcam("name")` | `terra = "webcam"` | V4L2 camera | | `hw.input("name")` | `terra = "input"` | Gamepad/joystick | | `hw.bluetooth("name")` | `terra = "bluetooth"` | BLE device | | `hw.net("name")` | `terra = "velodyne"` etc. | TCP/UDP device | | `hw.ethercat("name")` | `terra = "ethercat"` | EtherCAT bus | | `hw.spi("name")` | `terra = "spi"` | SPI device | | `hw.adc("name")` | `terra = "adc"` | ADC channel | | `hw.raw("name")` | any | Escape hatch | All error with a clear message listing available drivers if the name is not found. ### Factory Accessors | Method | Config key | Returns | |--------|-----------|---------| | `hw.local("name")` | `node = "StructName"` | `Box<dyn Node>` | | `hw.package("name")` | `package = "crate-name"` | `Box<dyn Node>` | | `hw.node("name")` | any | `Box<dyn Node>` — works for all 3 sources | `.node()` is a generic accessor that dispatches based on driver type: - **Terra** → wraps in a stub node (use typed accessors for full hardware access) - **Package** → delegates to `.package()` - **Local** → delegates to `.local()` ### Introspection | Method | Returns | Description | |--------|---------|-------------| | `hw.list()` | `Vec<&str>` | Sorted list of configured driver names | | `hw.has("name")` | `bool` | Whether a driver is configured | | `hw.params("name")` | `Option<&DriverParams>` | Config params for a driver | | `hw.driver_type("name")` | `Option<&DriverType>` | Terra / Package / Local / Legacy | | `hw.topic_mapping("name")` | `Option<&TopicMapping>` | Topic config for auto-bridging | | `hw.len()` | `usize` | Number of configured drivers | | `hw.is_empty()` | `bool` | Whether no drivers are configured | ## `DriverHandle` Wraps a Terra driver's connection params. Pass to your node constructor. | Method | Returns | Description | |--------|---------|-------------| | `handle.params()` | `&DriverParams` | Config params from `[drivers.NAME]` | | `handle.terra_name()` | `Option<&str>` | Terra shortname (e.g., `"dynamixel"`) | ```rust let handle = hw.dynamixel("arm")?; let port: String = handle.params().get("port")?; let terra_name: Option<&str> = handle.terra_name(); // "dynamixel" ``` ## `TopicMapping` Optional topic configuration for auto-bridging drivers to HORUS topics. Set via `topic`, `topic_state`, `topic_command` fields in `[drivers.NAME]`: ```toml [drivers.imu] terra = "mpu6050" bus = "i2c-1" topic = "sensors/imu" # sensor data output [drivers.arm] terra = "dynamixel" port = "/dev/ttyUSB0" topic_state = "arm/joint_states" # state output topic_command = "arm/joint_commands" # command input ``` | Field | Description | |-------|-------------| | `topic` | Sensor data output topic (e.g., `"sensors/imu"`) | | `topic_state` | Joint state output topic (e.g., `"arm/joint_states"`) | | `topic_command` | Command input topic (e.g., `"arm/joint_commands"`) | Access via `hw.topic_mapping("name")`: ```rust if let Some(mapping) = hw.topic_mapping("arm") { if let Some(state_topic) = &mapping.topic_state { println!("State published on: {}", state_topic); } } ``` Topic fields are **not** included in `DriverParams` — they're bridge configuration, not device params. Used by bridge layers (e.g., `terra-horus`) to auto-create sensor/actuator forwarding. ## `DriverParams` Typed access to config values from a `[drivers.NAME]` table. | Method | Description | |--------|-------------| | `params.get::<String>("port")?` | Required param — errors if missing or wrong type | | `params.get::<u32>("baudrate")?` | Supports String, bool, i32, u32, u64, f32, f64, `Vec<T>` | | `params.get_or("timeout", 1000u32)` | Optional param — returns default if missing | | `params.has("key")` | Whether a key exists | | `params.keys()` | Iterator over param names | | `params.raw("key")` | Raw `toml::Value` for a key | ## `register_driver!` Register a local driver factory so `hw.local("name")` can instantiate it. ```rust use horus::prelude::*; use horus::drivers::{DriverParams, register_driver}; struct ConveyorDriver { port: Box<dyn serialport::SerialPort>, belt_length: u32, pub_state: Topic<ConveyorState>, } impl ConveyorDriver { fn from_params(params: &DriverParams) -> HorusResult<Self> { let port_name = params.get::<String>("port")?; let baud = params.get_or("baudrate", 57600u32); Ok(Self { port: serialport::new(&port_name, baud).open()?, belt_length: params.get_or("belt_length_mm", 2400), pub_state: Topic::new("conveyor/state")?, }) } } impl Node for ConveyorDriver { fn tick(&mut self) { let pos = self.read_encoder(); self.pub_state.send(&ConveyorState { position_mm: pos }); } fn enter_safe_state(&mut self) { self.write_speed(0.0); } } // One line — registers the factory register_driver!(ConveyorDriver, ConveyorDriver::from_params); ``` Then in `horus.toml`: ```toml [drivers.conveyor] node = "ConveyorDriver" port = "/dev/ttyACM0" baudrate = 57600 belt_length_mm = 2400 ``` And in `main.rs`: ```rust let mut hw = drivers::load()?; scheduler.add(hw.local("conveyor")?) .order(100) .rate(50_u64.hz()) .build()?; ``` ## `DriverType` Identifies where a driver comes from: ```rust match hw.driver_type("arm") { Some(DriverType::Terra(name)) => println!("Terra: {}", name), Some(DriverType::Package(pkg)) => println!("Package: {}", pkg), Some(DriverType::Local(node)) => println!("Local: {}", node), Some(DriverType::Legacy) => println!("Legacy feature flag"), None => println!("Not configured"), } ``` ## Adding to Scheduler `scheduler.add()` accepts both concrete types and `Box<dyn Node>`: ```rust // Concrete type — most common scheduler.add(ArmDriver::new(handle)).build()?; // Box<dyn Node> — from hw.local() or hw.package() scheduler.add(hw.local("conveyor")?).build()?; // Both use the same .add() — full builder API available scheduler.add(hw.local("sensor")?) .order(5) .rate(500_u64.hz()) .on_miss(Miss::Skip) .failure_policy(FailurePolicy::restart(3, 100_u64.ms())) .build()?; ``` ## Complete Example ```toml # horus.toml [package] name = "my-robot" version = "0.1.0" [drivers.arm] terra = "dynamixel" port = "/dev/ttyUSB0" baudrate = 1000000 servo_ids = [1, 2, 3, 4, 5, 6] [drivers.lidar] terra = "rplidar" port = "/dev/ttyUSB1" [drivers.imu] terra = "bno055" bus = "i2c-1" address = 0x28 ``` ```rust use horus::prelude::*; use horus::drivers; fn main() -> HorusResult<()> { let mut hw = drivers::load()?; let mut sched = Scheduler::new() .tick_rate(1000_u64.hz()) .prefer_rt() .watchdog(200_u64.ms()); sched.add(ArmDriver::new(hw.dynamixel("arm")?)) .order(0).rate(500_u64.hz()) .on_miss(Miss::SafeMode) .build()?; sched.add(LidarDriver::new(hw.rplidar("lidar")?)) .order(10).rate(40_u64.hz()) .on_miss(Miss::Skip) .build()?; sched.add(ImuDriver::new(hw.i2c("imu")?)) .order(5).rate(100_u64.hz()) .build()?; sched.add(Planner::new()) .order(50).rate(10_u64.hz()).compute() .build()?; sched.run() } ``` ## Testing with Mock Drivers Use `terra = "virtual"` in a test config: ```toml # tests/test_drivers.toml [drivers.arm] terra = "virtual" joints = 6 [drivers.lidar] terra = "virtual" type = "lidar" ``` ```rust #[test] fn robot_initializes() { let mut hw = drivers::load_from("tests/test_drivers.toml").unwrap(); let mut sched = Scheduler::new().deterministic(true); sched.add(ArmDriver::new(hw.dynamixel("arm").unwrap())) .build().unwrap(); sched.tick_once().unwrap(); } ``` --- ## Data Types & Encoding Path: /rust/api/tensor-messages Description: Image, PointCloud, and DepthImage types for robotics data # Data Types & Encoding High-level types for camera images, 3D point clouds, and depth maps. These types use zero-copy shared memory transport internally but expose ergonomic, domain-specific APIs. ```rust use horus::prelude::*; // Provides Image, PointCloud, DepthImage, TensorDtype, Device ``` ## Image Represents a camera frame with pixel-level access and encoding metadata. ```rust // Create a 1080p RGB image let image = Image::new(1920, 1080, ImageEncoding::Rgb8); // Publish on a topic let topic: Topic = Topic::new("camera.rgb")?; topic.send(&image); // Receive and access pixels if let Some(img) = topic.recv() { println!("{}x{}, encoding: {:?}", img.width(), img.height(), img.encoding()); let pixel = img.pixel(100, 200); } ``` ### ImageEncoding | Encoding | Channels | Dtype | Description | |----------|----------|-------|-------------| | `Rgb8` | 3 | U8 | Standard RGB color | | `Rgba8` | 4 | U8 | RGB with alpha | | `Bgr8` | 3 | U8 | BGR (OpenCV default) | | `Bgra8` | 4 | U8 | BGR with alpha | | `Mono8` | 1 | U8 | Grayscale 8-bit | | `Mono16` | 1 | U16 | Grayscale 16-bit | ## PointCloud Represents a 3D point cloud with per-point field access. ```rust // Create a point cloud with XYZ + intensity fields let cloud = PointCloud::new(10_000, &["x", "y", "z", "intensity"], TensorDtype::F32); // Publish let topic: Topic = Topic::new("lidar.points")?; topic.send(&cloud); // Receive and iterate points if let Some(pc) = topic.recv() { println!("{} points, {} fields", pc.num_points(), pc.fields().len()); for i in 0..pc.num_points() { let x = pc.field_f32("x", i); let y = pc.field_f32("y", i); let z = pc.field_f32("z", i); } } ``` ## DepthImage Represents a depth map, typically from an RGBD or stereo camera. ```rust // Create a 640x480 depth image with millimeter-precision U16 values let depth = DepthImage::millimeters(640, 480); // Publish let topic: Topic = Topic::new("camera.depth")?; topic.send(&depth); // Receive and query depth if let Some(d) = topic.recv() { let depth_mm = d.depth_at(320, 240); println!("Center depth: {} mm", depth_mm); } ``` ## TensorDtype Element data type used when constructing PointCloud, DepthImage, and other tensor-backed types. | Dtype | Size | Use Case | |-------|------|----------| | F32 | 4 | ML training/inference | | F64 | 8 | High-precision computation | | F16 | 2 | Memory-efficient inference | | BF16 | 2 | Training on modern GPUs | | U8 | 1 | Images | | U16 | 2 | Depth sensors (mm) | | U32 | 4 | Large indices | | U64 | 8 | Counters, timestamps | | I8 | 1 | Quantized inference | | I16 | 2 | Audio, sensor data | | I32 | 4 | General integer | | I64 | 8 | Large signed values | | Bool | 1 | Masks | ### TensorDtype Methods ```rust let dtype = TensorDtype::F32; assert_eq!(dtype.element_size(), 4); assert!(dtype.is_float()); assert!(!dtype.is_signed_int()); println!("{}", dtype); // "f32" // DLPack interop let dl = dtype.to_dlpack(); let back = TensorDtype::from_dlpack(dl.0, dl.1).unwrap(); // Parse from string let parsed = TensorDtype::parse("float32").unwrap(); ``` | Method | Returns | Description | |--------|---------|-------------| | `.element_size()` | `usize` | Bytes per element | | `.is_float()` | `bool` | Whether this is a floating-point type (F16, BF16, F32, F64) | | `.is_signed_int()` | `bool` | Whether this is a signed integer type (I8, I16, I32, I64) | | `TensorDtype::parse(s)` | `Option` | Parse from string ("float32", "uint8", "int16", etc.) | | `.to_dlpack()` | `(u8, u8)` | Convert to DLPack type code and bits | | `TensorDtype::from_dlpack(code, bits)` | `Option` | Create from DLPack type code and bits | ## Device The `Device` struct is a device location descriptor that tags tensors with a target device. `Device::cuda(N)` creates a descriptor — actual GPU tensor pools are not yet implemented. ```rust Device::cpu() // CPU / shared memory Device::cuda(0) // CUDA device descriptor (metadata only) // Parse from string let cpu = Device::parse("cpu").unwrap(); let dev = Device::parse("cuda:0").unwrap(); // Check device type assert!(Device::cpu().is_cpu()); assert!(Device::cuda(0).is_cuda()); ``` | Method | Returns | Description | |--------|---------|-------------| | `Device::cpu()` | `Device` | CPU device | | `Device::cuda(index)` | `Device` | CUDA GPU device with the given index | | `.is_cpu()` | `bool` | Whether this is a CPU device | | `.is_cuda()` | `bool` | Whether this is a CUDA device | | `Device::parse(s)` | `Option` | Parse from string ("cpu" or "cuda:0") | ## Python Interop Image, PointCloud, and DepthImage are available in Python with NumPy zero-copy access. ```python import horus import numpy as np # Subscribe to camera images topic = horus.Topic("camera.rgb", horus.Image) img = topic.recv() if img is not None: print(f"{img.width}x{img.height}, encoding: {img.encoding}") arr = img.to_numpy() # Zero-copy NumPy view # Subscribe to point clouds pc_topic = horus.Topic("lidar.points", horus.PointCloud) pc = pc_topic.recv() if pc is not None: points = pc.to_numpy() # (N, fields) NumPy array print(f"{pc.num_points} points") # Subscribe to depth images depth_topic = horus.Topic("camera.depth", horus.DepthImage) depth = depth_topic.recv() if depth is not None: depth_arr = depth.to_numpy() # (H, W) NumPy array ``` ## See Also - [Tensor](/rust/api/tensor) — Low-level tensor descriptor for ML pipelines - [Message Types](/concepts/message-types) — All HORUS message types - [Python Memory Types](/python/api/memory-types) — Image, PointCloud, DepthImage with NumPy/PyTorch/JAX interop --- ## Basic Examples Path: /rust/examples/basic-examples Description: Simple HORUS patterns for beginners # Basic Examples Learn HORUS fundamentals through simple, focused examples. Each example is complete and runnable with `horus run`. **Estimated time**: 30-45 minutes ## Prerequisites - HORUS installed ([Installation Guide](/getting-started/installation)) - Completed [Quick Start](/getting-started/quick-start) - Basic Rust knowledge --- ## 1. Basic Publisher-Subscriber The foundational pattern in HORUS: one node publishes data, another subscribes. ### Publisher Node **File: `publisher.rs`** ```rust use horus::prelude::*; // Define publisher node struct SensorNode { data_pub: Topic, counter: f32, } impl SensorNode { fn new() -> Result { Ok(Self { data_pub: Topic::new("sensor_data")?, counter: 0.0, }) } } impl Node for SensorNode { fn name(&self) -> &str { "SensorNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Sensor initialized"); Ok(()) } fn tick(&mut self) { // Simulate sensor reading let reading = self.counter.sin() * 10.0; // Publish data self.data_pub.send(reading); hlog!(debug, "Published: {:.2}", reading); self.counter += 0.1; } // SAFETY: no actuators — shutdown is optional for pure sensor nodes fn shutdown(&mut self) -> Result<()> { hlog!(info, "Sensor shutdown"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new()?).order(0).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run publisher.rs ``` ### Subscriber Node **File: `subscriber.rs`** ```rust use horus::prelude::*; struct ProcessorNode { data_sub: Topic, } impl ProcessorNode { fn new() -> Result { Ok(Self { data_sub: Topic::new("sensor_data")?, }) } } impl Node for ProcessorNode { fn name(&self) -> &str { "ProcessorNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Processor initialized"); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the buffer and avoid stale data if let Some(data) = self.data_sub.recv() { // Process received data let processed = data * 2.0; hlog!(debug, "Processed: {:.2}", processed); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Processor shutdown"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(ProcessorNode::new()?).order(0).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: HORUS uses a **flat namespace** (like ROS), so processes automatically share topics: ```bash # Terminal 1 horus run publisher.rs # Terminal 2 (automatically connects!) horus run subscriber.rs ``` Both use the same topic name (`"sensor_data"`) → communication works automatically! ### Combined Application **File: `pubsub.rs`** ```rust use horus::prelude::*; // Publisher struct SensorNode { data_pub: Topic, counter: f32, } impl Node for SensorNode { fn name(&self) -> &str { "SensorNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Sensor online"); Ok(()) } fn tick(&mut self) { let reading = self.counter.sin() * 10.0; self.data_pub.send(reading); self.counter += 0.1; } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Sensor offline"); Ok(()) } } // Subscriber struct ProcessorNode { data_sub: Topic, } impl Node for ProcessorNode { fn name(&self) -> &str { "ProcessorNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Processor online"); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the buffer if let Some(data) = self.data_sub.recv() { let processed = data * 2.0; hlog!(info, "Received: {:.2} -> {:.2}", data, processed); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Processor offline"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Add both nodes scheduler.add(SensorNode { data_pub: Topic::new("sensor_data")?, counter: 0.0, }).order(0).build()?; scheduler.add(ProcessorNode { data_sub: Topic::new("sensor_data")?, }).order(1).build()?; // Run both nodes together scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run pubsub.rs ``` **Key Concepts**: - Publisher uses `Topic::new("topic")` to create publisher - Subscriber uses same topic name `"sensor_data"` - Priority matters: Publisher (0) runs before Subscriber (1) - `recv()` returns `Option` - handle None gracefully --- ## 2. Robot Velocity Controller Control a robot using standard CmdVel messages. **File: `robot_controller.rs`** ```rust use horus::prelude::*; // Keyboard input velocity commands struct TeleopNode { cmd_pub: Topic, } impl TeleopNode { fn new() -> Result { Ok(Self { cmd_pub: Topic::new("cmd_vel")?, }) } } impl Node for TeleopNode { fn name(&self) -> &str { "TeleopNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Teleop ready - sending movement commands"); Ok(()) } fn tick(&mut self) { // Simulate keyboard input (w/a/s/d) // In real code, read from device::Input let cmd = CmdVel::new(1.0, 0.5); // Forward + turn right self.cmd_pub.send(cmd); } // SAFETY: send zero velocity on shutdown to stop the robot fn shutdown(&mut self) -> Result<()> { // CRITICAL: send stop command before exiting — prevents runaway let stop = CmdVel::zero(); self.cmd_pub.send(stop); hlog!(info, "Teleop stopped"); Ok(()) } } // Velocity commands motor control struct MotorDriverNode { cmd_sub: Topic, } impl MotorDriverNode { fn new() -> Result { Ok(Self { cmd_sub: Topic::new("cmd_vel")?, }) } } impl Node for MotorDriverNode { fn name(&self) -> &str { "MotorDriverNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Motor driver initialized"); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the command buffer if let Some(cmd) = self.cmd_sub.recv() { // Convert to differential drive (left/right wheel speeds) let left_speed = cmd.linear - cmd.angular; let right_speed = cmd.linear + cmd.angular; // Send to motors hlog!(debug, "Motors: L={:.2} m/s, R={:.2} m/s", left_speed, right_speed); // In real code: send to hardware // motor_driver.set_speeds(left_speed, right_speed)?; } } // SAFETY: stop motors on shutdown — in real code, send zero to hardware here fn shutdown(&mut self) -> Result<()> { hlog!(info, "Motors stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(TeleopNode::new()?).order(0).build()?; scheduler.add(MotorDriverNode::new()?).order(1).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run robot_controller.rs ``` **Key Concepts**: - `CmdVel` is a standard robotics message type - `CmdVel::new(linear, angular)` creates velocity commands - Differential drive: `left = linear - angular`, `right = linear + angular` - Use `shutdown()` to send safe stop commands --- ## 3. Lidar Obstacle Detection Process laser scan data to detect obstacles and stop the robot. **File: `obstacle_detector.rs`** ```rust use horus::prelude::*; // Lidar Scan data struct LidarNode { scan_pub: Topic, angle: f32, } impl LidarNode { fn new() -> Result { Ok(Self { scan_pub: Topic::new("scan")?, angle: 0.0, }) } } impl Node for LidarNode { fn name(&self) -> &str { "LidarNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Lidar initialized"); Ok(()) } fn tick(&mut self) { // NOTE: in real code, read from hardware instead of simulating let mut scan = LaserScan::new(); // Simulate lidar readings (sine wave for demo) for i in 0..360 { scan.ranges[i] = 5.0 + (self.angle + i as f32 * 0.01).sin() * 2.0; } // Add one close obstacle in front scan.ranges[0] = 0.3; // 30cm directly ahead! self.scan_pub.send(scan); self.angle += 0.1; } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Lidar offline"); Ok(()) } } // Scan data Obstacle detection Stop command struct ObstacleDetector { scan_sub: Topic, cmd_pub: Topic, safety_distance: f32, } impl ObstacleDetector { fn new(safety_distance: f32) -> Result { Ok(Self { scan_sub: Topic::new("scan")?, cmd_pub: Topic::new("cmd_vel")?, safety_distance, }) } } impl Node for ObstacleDetector { fn name(&self) -> &str { "ObstacleDetector" } fn init(&mut self) -> Result<()> { hlog!(info, "Obstacle detector active - safety distance: {:.2}m", self.safety_distance); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick — stale scans cause delayed obstacle detection if let Some(scan) = self.scan_sub.recv() { // Find minimum distance if let Some(min_dist) = scan.min_range() { if min_dist < self.safety_distance { // SAFETY: emergency stop — send zero velocity immediately let stop = CmdVel::zero(); self.cmd_pub.send(stop); hlog!(warn, "[WARNING] Obstacle detected at {:.2}m - STOPPING!", min_dist); } else { // Safe to move hlog!(debug, "Safe - closest obstacle: {:.2}m", min_dist); } } } } // SAFETY: send stop command on shutdown as a safety precaution fn shutdown(&mut self) -> Result<()> { self.cmd_pub.send(CmdVel::zero()); hlog!(info, "Obstacle detector offline"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(LidarNode::new()?).order(0).build()?; // Obstacle detector runs with HIGH priority (1) scheduler.add(ObstacleDetector::new(0.5)?).order(1).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run obstacle_detector.rs ``` **Key Concepts**: - `LaserScan` has 360 range readings (one per degree) - `scan.min_range()` finds closest obstacle - `scan.is_range_valid(index)` checks if reading is good - Safety nodes should run at HIGH priority --- ## 4. PID Controller Implement a PID controller for position tracking. **File: `pid_controller.rs`** ```rust use horus::prelude::*; struct PIDController { setpoint_sub: Topic, // Desired position feedback_sub: Topic, // Current position output_pub: Topic, // Control output kp: f32, // Proportional gain ki: f32, // Integral gain kd: f32, // Derivative gain integral: f32, last_error: f32, } impl PIDController { fn new(kp: f32, ki: f32, kd: f32) -> Result { Ok(Self { setpoint_sub: Topic::new("setpoint")?, feedback_sub: Topic::new("feedback")?, output_pub: Topic::new("control_output")?, kp, ki, kd, integral: 0.0, last_error: 0.0, }) } } impl Node for PIDController { fn name(&self) -> &str { "PIDController" } fn init(&mut self) -> Result<()> { hlog!(info, "PID initialized - Kp: {}, Ki: {}, Kd: {}", self.kp, self.ki, self.kd); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() on ALL subscribed topics every tick let setpoint = self.setpoint_sub.recv().unwrap_or(0.0); let feedback = self.feedback_sub.recv().unwrap_or(0.0); // Calculate error let error = setpoint - feedback; // Integral term (accumulated error) self.integral += error; // Derivative term (rate of change) let derivative = error - self.last_error; // PID output let output = self.kp * error + self.ki * self.integral + self.kd * derivative; // Publish control output self.output_pub.send(output); hlog!(debug, "PID: setpoint={:.2}, feedback={:.2}, error={:.2}, output={:.2}", setpoint, feedback, error, output); // Update state self.last_error = error; } // SAFETY: send zero output on shutdown to stop actuators downstream fn shutdown(&mut self) -> Result<()> { self.output_pub.send(0.0); hlog!(info, "PID controller stopped"); Ok(()) } } // Simple test system node struct TestSystem { output_sub: Topic, feedback_pub: Topic, position: f32, } impl TestSystem { fn new() -> Result { Ok(Self { output_sub: Topic::new("control_output")?, feedback_pub: Topic::new("feedback")?, position: 0.0, }) } } impl Node for TestSystem { fn name(&self) -> &str { "TestSystem" } fn tick(&mut self) { // IMPORTANT: call recv() every tick to consume control commands if let Some(output) = self.output_sub.recv() { self.position += output * 0.01; // Simple integration } // Publish current position self.feedback_pub.send(self.position); } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Test system stopped"); Ok(()) } } // Setpoint generator struct SetpointNode { setpoint_pub: Topic, } impl Node for SetpointNode { fn name(&self) -> &str { "SetpointNode" } fn tick(&mut self) { // Target position let setpoint = 10.0; self.setpoint_pub.send(setpoint); } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Setpoint generator stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Setpoint generator scheduler.add(SetpointNode { setpoint_pub: Topic::new("setpoint")?, }).order(0).build()?; // Test system (simulates plant) scheduler.add(TestSystem::new()?).order(1).build()?; // PID controller (Kp=0.5, Ki=0.01, Kd=0.1) scheduler.add(PIDController::new(0.5, 0.01, 0.1)?).order(2).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run pid_controller.rs ``` **Key Concepts**: - PID = Proportional + Integral + Derivative - Proportional: immediate response to error - Integral: corrects accumulated error - Derivative: dampens oscillations - Tune gains (Kp, Ki, Kd) for your system --- ## 5. Multi-Node Pipeline Chain multiple processing stages together. **File: `pipeline.rs`** ```rust use horus::prelude::*; // Stage 1: Data acquisition struct SensorNode { raw_pub: Topic, counter: f32, } impl SensorNode { fn new() -> Result { Ok(Self { raw_pub: Topic::new("raw_data")?, counter: 0.0, }) } } impl Node for SensorNode { fn name(&self) -> &str { "SensorNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Stage 1: Sensor online"); Ok(()) } fn tick(&mut self) { // Simulate noisy sensor let raw_data = 50.0 + (self.counter * 0.5).sin() * 20.0; self.raw_pub.send(raw_data); hlog!(debug, "Raw: {:.2}", raw_data); self.counter += 0.1; } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Sensor offline"); Ok(()) } } // Stage 2: Filtering struct FilterNode { raw_sub: Topic, filtered_pub: Topic, alpha: f32, // Low-pass filter coefficient filtered_value: f32, } impl FilterNode { fn new(alpha: f32) -> Result { Ok(Self { raw_sub: Topic::new("raw_data")?, filtered_pub: Topic::new("filtered_data")?, alpha, filtered_value: 0.0, }) } } // PATTERN: Pipeline stage — subscribe, transform, republish impl Node for FilterNode { fn name(&self) -> &str { "FilterNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Stage 2: Filter online"); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the raw data buffer if let Some(raw) = self.raw_sub.recv() { // Exponential moving average self.filtered_value = self.alpha * raw + (1.0 - self.alpha) * self.filtered_value; self.filtered_pub.send(self.filtered_value); hlog!(debug, "Filtered: {:.2}", self.filtered_value); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Filter offline"); Ok(()) } } // Stage 3: Decision making struct ControllerNode { filtered_sub: Topic, cmd_pub: Topic, threshold: f32, } impl ControllerNode { fn new(threshold: f32) -> Result { Ok(Self { filtered_sub: Topic::new("filtered_data")?, cmd_pub: Topic::new("commands")?, threshold, }) } } impl Node for ControllerNode { fn name(&self) -> &str { "ControllerNode" } fn init(&mut self) -> Result<()> { hlog!(info, "Stage 3: Controller online"); Ok(()) } fn tick(&mut self) { // IMPORTANT: call recv() every tick to consume filtered data if let Some(value) = self.filtered_sub.recv() { let command = if value > self.threshold { 1.0 } else { 0.0 }; self.cmd_pub.send(command); hlog!(info, "Value: {:.2}, Threshold: {:.2}, Command: {:.0}", value, self.threshold, command); } } // SAFETY: send zero command on shutdown to stop downstream actuators fn shutdown(&mut self) -> Result<()> { self.cmd_pub.send(0.0); hlog!(info, "Controller offline"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Add pipeline stages in priority order scheduler.add(SensorNode::new()?).order(0).build()?; scheduler.add(FilterNode::new(0.2)?).order(1).build()?; scheduler.add(ControllerNode::new(50.0)?).order(2).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run pipeline.rs ``` **Key Concepts**: - Data flows: Sensor Filter Controller - Each stage has different priority (0, 1, 2) - Exponential moving average: `filtered = α * new + (1-α) * old` - Priorities ensure correct execution order --- ## 6. Camera Image Pipeline Send and receive camera images using `Image` with zero-copy shared memory. ### Camera Sender **File: `camera_sender.rs`** ```rust use horus::prelude::*; struct CameraSender { topic: Topic, } impl Node for CameraSender { fn name(&self) -> &str { "CameraSender" } fn tick(&mut self) { // NOTE: Image::new() allocates from a global shared memory pool — zero-copy transport let mut img = Image::new(480, 640, ImageEncoding::Rgb8) .expect("failed to allocate image"); // Fill with a solid color (blue) img.fill(&[0, 0, 255]); // Set a red pixel at (100, 200) img.set_pixel(100, 200, &[255, 0, 0]); // Send — only a lightweight descriptor is transmitted. // The pixel data stays in shared memory (zero-copy). self.topic.send(&img); hlog!(debug, "Sent {}x{} image", img.width(), img.height()); } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Camera sender stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(CameraSender { topic: Topic::new("camera.rgb")?, }).order(0).build()?; scheduler.run()?; Ok(()) } ``` ### Camera Receiver **File: `camera_receiver.rs`** ```rust use horus::prelude::*; struct CameraReceiver { topic: Topic, } impl Node for CameraReceiver { fn name(&self) -> &str { "CameraReceiver" } fn tick(&mut self) { // IMPORTANT: call recv() every tick — stale images waste shared memory pool slots if let Some(img) = self.topic.recv() { // Read a pixel if let Some(px) = img.pixel(100, 200) { hlog!(info, "Pixel at (100,200): R={} G={} B={}", px[0], px[1], px[2]); } hlog!(debug, "Received {}x{} {:?} image ({} bytes)", img.width(), img.height(), img.encoding(), img.data().len()); } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Camera receiver stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(CameraReceiver { topic: Topic::new("camera.rgb")?, }).order(0).build()?; scheduler.run()?; Ok(()) } ``` **Run it** (two terminals): ```bash # Terminal 1 horus run camera_sender.rs # Terminal 2 horus run camera_receiver.rs ``` **Key Concepts**: - `Image::new(width, height, encoding)` allocates from a global shared memory pool - `topic.send(&img)` sends only a lightweight descriptor; pixel data stays in shared memory - `topic.recv()` returns `Option` — zero-copy access to the sender's data - `pixel()` / `set_pixel()` / `fill()` / `roi()` for pixel-level access --- ## 7. Point Cloud Processing Create, send, and process 3D point clouds. **File: `pointcloud_demo.rs`** ```rust use horus::prelude::*; struct PointCloudSender { topic: Topic, } impl Node for PointCloudSender { fn name(&self) -> &str { "PointCloudSender" } fn tick(&mut self) { // NOTE: PointCloud::new() allocates from the shared memory pool — zero-copy transport let mut pc = PointCloud::from_xyz(\&points) // 1000 points, 3 fields .expect("failed to allocate point cloud"); // Fill with sample data — a sphere of radius 1.0 let floats = pc.data_mut_as::(); for i in 0..1000 { let theta = (i as f32 / 1000.0) * std::f32::consts::TAU; let phi = (i as f32 / 1000.0) * std::f32::consts::PI; floats[i * 3] = phi.sin() * theta.cos(); // x floats[i * 3 + 1] = phi.sin() * theta.sin(); // y floats[i * 3 + 2] = phi.cos(); // z } pc.set_frame_id("lidar_frame"); self.topic.send(&pc); hlog!(debug, "Sent {} points", pc.point_count()); } fn shutdown(&mut self) -> Result<()> { hlog!(info, "PointCloud sender stopped"); Ok(()) } } struct PointCloudReceiver { topic: Topic, } impl Node for PointCloudReceiver { fn name(&self) -> &str { "PointCloudReceiver" } fn tick(&mut self) { // IMPORTANT: call recv() every tick — stale point clouds waste shared memory pool slots if let Some(pc) = self.topic.recv() { // Extract all XYZ points as Vec<[f32; 3]> if let Some(points) = pc.extract_xyz() { let centroid = points.iter().fold([0.0f32; 3], |acc, p| { [acc[0] + p[0], acc[1] + p[1], acc[2] + p[2]] }); let n = points.len() as f32; hlog!(info, "Received {} points, centroid: ({:.2}, {:.2}, {:.2})", points.len(), centroid[0] / n, centroid[1] / n, centroid[2] / n); } } } fn shutdown(&mut self) -> Result<()> { hlog!(info, "PointCloud receiver stopped"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(PointCloudSender { topic: Topic::new("lidar.points")?, }).order(0).build()?; scheduler.add(PointCloudReceiver { topic: Topic::new("lidar.points")?, }).order(1).build()?; scheduler.run()?; Ok(()) } ``` **Run it**: ```bash horus run pointcloud_demo.rs ``` **Key Concepts**: - `PointCloud::new(num_points, fields_per_point, dtype)` — 3=XYZ, 4=XYZI, 6=XYZRGB - `extract_xyz()` returns `Option>` for F32 clouds - `point_count()`, `fields_per_point()`, `is_xyz()` for metadata - Uses the same zero-copy shared memory transport as Image --- ## Next Steps Now that you understand basic patterns, explore: - [Second Application Tutorial](/getting-started/second-application) - Build a 3-node sensor pipeline - [Advanced Examples](/rust/examples/advanced-examples) - Multi-process systems, Python integration - [Testing](/development/testing) - Write tests for your nodes - [Using Prebuilt Nodes](/package-management/using-prebuilt-nodes) - Leverage the standard library - [Message Types](/concepts/message-types) - Complete message reference Stuck? Check: - [Troubleshooting](/troubleshooting) - Fix common issues - [Monitor](/development/monitor) - Debug with the web UI --- ## Topic API Path: /rust/api/topic Description: Zero-copy pub/sub communication channels between HORUS nodes # Topic API `Topic` is the primary communication primitive in HORUS. It provides typed publish/subscribe channels backed by shared memory for zero-copy IPC within a single machine. ## Creating a Topic ```rust use horus::prelude::*; // Default capacity and slot size let topic = Topic::::new("cmd_vel"); // Custom capacity (number of slots) and slot size (bytes per slot) let topic = Topic::::with_capacity("scan", 8, 4096); ``` | Constructor | Returns | Description | |-------------|---------|-------------| | `Topic::::new(name)` | `Topic` | Create with default capacity and slot size | | `Topic::::with_capacity(name, capacity, slot_size)` | `Topic` | Create with explicit capacity and slot size | > Most users only need `Topic::new(name)`. Use `with_capacity` only if you need to tune buffer sizes for very large or very small messages. The `name` string identifies the shared-memory channel. Two `Topic` instances with the same name and type connect automatically — one publishes, the other subscribes. Backend selection (in-process ring buffer vs cross-process SHM) is automatic based on whether publisher and subscriber live in the same process. --- ## Sending Messages | Method | Returns | Description | |--------|---------|-------------| | `send(msg)` | `()` | Publish a message (non-blocking, drops oldest if full) | | `try_send(msg)` | `Result<(), T>` | Publish if space available; returns message back on failure | | `send_blocking(msg, timeout)` | `Result<(), SendBlockingError>` | Block until space is available or timeout elapses | ### SendBlockingError | Variant | Description | |---------|-------------| | `Timeout` | Timed out waiting for space in the ring buffer | ### Example ```rust let topic = Topic::::new("cmd_vel"); // Fire-and-forget (overwrites oldest if full) topic.send(CmdVel { linear: 1.0, angular: 0.0 }); // Try without blocking match topic.try_send(CmdVel { linear: 0.5, angular: 0.1 }) { Ok(()) => { /* sent */ } Err(msg) => { /* buffer full, msg returned */ } } // Block up to 10ms use horus::prelude::*; match topic.send_blocking(CmdVel { linear: 1.0, angular: 0.0 }, 10_u64.ms()) { Ok(()) => { /* sent */ } Err(SendBlockingError::Timeout) => { /* timed out */ } } ``` --- ## Receiving Messages | Method | Returns | Description | |--------|---------|-------------| | `recv()` | `Option` | Take the next unread message (FIFO order) | | `read_latest()` | `Option` | Read the most recent message, skipping older ones (requires `T: Copy`) | `recv()` returns messages in order. Each message is delivered to each subscriber exactly once. `read_latest()` is useful for state-like data (sensor readings, poses) where you only care about the newest value. ### Example ```rust let topic = Topic::::new("imu"); // IMPORTANT: call recv() every tick to drain the buffer and avoid stale data accumulation // Process all pending messages while let Some(msg) = topic.recv() { println!("Accel: ({}, {}, {})", msg.linear_accel.x, msg.linear_accel.y, msg.linear_accel.z); } // NOTE: read_latest() requires T: Copy — skips older messages and returns only the newest if let Some(latest) = topic.read_latest() { println!("Latest orientation: {:?}", latest.orientation); } ``` --- ## State & Metrics | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&str` | The topic name | | `has_message()` | `bool` | Whether there is at least one unread message | | `pending_count()` | `usize` | Number of unread messages in the buffer | | `dropped_count()` | `u64` | Total failed send attempts (includes buffer-full overwrites) | | `metrics()` | `TopicMetrics` | Aggregate send/receive statistics | ### TopicMetrics | Method | Returns | Description | |--------|---------|-------------| | `messages_sent()` | `u64` | Total messages published on this topic | | `messages_received()` | `u64` | Total messages consumed from this topic | | `send_failures()` | `u64` | Failed send attempts (e.g., `try_send` on a full buffer) | | `recv_failures()` | `u64` | Failed receive attempts (e.g., `recv` on empty buffer) | ### Example ```rust let topic = Topic::::new("cmd_vel"); // Check state if topic.has_message() { println!("{} messages pending", topic.pending_count()); } // Check for dropped messages if topic.dropped_count() > 0 { hlog!(warn, "Dropped {} messages", topic.dropped_count()); } // Check metrics let m = topic.metrics(); println!("Sent: {}, Received: {}", m.messages_sent(), m.messages_received()); ``` --- ## Pool-Backed Types (Zero-Copy) For large data types (`Image`, `PointCloud`, `DepthImage`, `Tensor`), HORUS uses pool-backed allocation to avoid copying payloads through the ring buffer. The `Topic` API provides specialized methods for these types. ### Image Topics ```rust let topic = Topic::::new("camera/rgb"); // Send (moves the Image into the pool slot) topic.send(image); // Try send (returns the Image on failure) match topic.try_send(image) { Ok(()) => {} Err(img) => { /* buffer full */ } } // IMPORTANT: call recv() every tick to drain the buffer — images are large and stale frames waste pool slots if let Some(img) = topic.recv() { println!("{}x{} image received", img.width(), img.height()); } ``` ### PointCloud Topics ```rust let topic = Topic::::new("lidar/points"); topic.send(cloud); // IMPORTANT: call recv() every tick to avoid stale point clouds filling the pool if let Some(cloud) = topic.recv() { println!("{} points received", cloud.len()); } ``` ### DepthImage Topics ```rust let topic = Topic::::new("camera/depth"); topic.send(depth); // IMPORTANT: call recv() every tick to drain depth frames if let Some(depth) = topic.recv() { println!("{}x{} depth image", depth.width(), depth.height()); } ``` ### Tensor Topics For advanced ML pipelines, you can use `Topic` directly. The same zero-copy transport that backs `Image`, `PointCloud`, and `DepthImage` is available for raw tensor data. See the [Tensor Messages](/rust/api/tensor-messages) page for details. ```rust let topic = Topic::::new("model/output"); // Send and receive work the same as any other topic type topic.send(tensor); // IMPORTANT: call recv() every tick to avoid stale tensor data in the pool if let Some(t) = topic.recv() { println!("Tensor shape: {:?}", t.shape()); } ``` For most use cases, prefer the domain types above (`Image`, `PointCloud`, `DepthImage`) — they provide convenience methods like `pixel()`, `point_at()`, and `get_depth()` while using the same zero-copy path internally. --- ## Detailed Method Reference --- ### new ```rust pub fn new(name: &str) -> Topic ``` Create a topic with default capacity and slot size. **Parameters:** - `name: &str` — Topic identifier. Two `Topic` instances with the same name and matching type `T` connect automatically. Names are case-sensitive. Use `/`-delimited hierarchical names: `"sensors/lidar/scan"`. **Returns:** `Topic` connected to the named shared memory channel. **Defaults:** - Capacity: 4 slots (ring buffer) - Slot size: `size_of::()` rounded up to page alignment **Backend auto-selection:** If publisher and subscriber are in the same process, uses an in-process ring buffer (no SHM overhead). Cross-process uses SHM. **Example:** ```rust let pub_topic = Topic::::new("cmd_vel"); let sub_topic = Topic::::new("cmd_vel"); // connects to same channel ``` --- ### send ```rust pub fn send(&self, msg: T) ``` Publish a message. **Non-blocking. Overwrites oldest if buffer is full.** **Parameters:** - `msg: T` — The message to publish. Moved into the ring buffer. **Buffer full behavior:** If all slots are occupied, the oldest unread message is silently overwritten. No error is returned. Use `dropped_count()` to detect this. **When to use:** Default for most real-time robotics — you always want the latest data, and blocking is unacceptable in a control loop. **Example:** ```rust topic.send(CmdVel::new(1.0, 0.0)); // Check if messages were dropped if topic.dropped_count() > 0 { hlog!(warn, "{} messages dropped — subscriber too slow", topic.dropped_count()); } ``` --- ### recv ```rust pub fn recv(&self) -> Option ``` Take the next unread message in FIFO order. **Returns:** - `Some(T)` — The oldest unread message (removed from buffer) - `None` — No unread messages available **Important:** Call `recv()` every tick in your node to drain the buffer. Failing to drain causes stale data accumulation and eventually message drops. **Delivery guarantee:** Each message is delivered to each subscriber exactly once. After `recv()` returns it, the message is consumed. **Example:** ```rust // Process ALL pending messages each tick while let Some(scan) = self.scan_topic.recv() { self.process(scan); } ``` --- ### read_latest ```rust pub fn read_latest(&self) -> Option where T: Copy ``` Read the most recent message, skipping all older ones. **Returns:** - `Some(T)` — Copy of the newest message - `None` — No messages available **Type constraint:** Requires `T: Copy` because it reads without consuming — multiple calls return the same value until a new message arrives. **When to use:** State-like data where you only care about the current value: poses, sensor readings, configuration. NOT for command streams where every message matters. **Example:** ```rust // Good: pose is state — only the latest matters if let Some(pose) = self.odom_topic.read_latest() { self.current_position = pose; } // Bad: cmd_vel is a command — you'd miss intermediate commands // Use recv() instead for command streams ``` --- ## Using Topics in Nodes Topics are typically used inside `Node` implementations registered with a `Scheduler`: ```rust use horus::prelude::*; struct LidarProcessor { scan_in: Topic, cmd_out: Topic, } impl LidarProcessor { fn new() -> Self { Self { scan_in: Topic::new("scan"), cmd_out: Topic::new("cmd_vel"), } } } impl Node for LidarProcessor { fn name(&self) -> &str { "LidarProcessor" } fn tick(&mut self) { // IMPORTANT: call recv() every tick to drain the scan buffer if let Some(scan) = self.scan_in.recv() { // Find closest obstacle let min_range = scan.ranges.iter() .copied() .filter(|r| *r > scan.range_min && *r < scan.range_max) .fold(f32::MAX, f32::min); // SAFETY: send zero velocity when obstacle is too close let speed = if min_range < 0.5 { 0.0 } else { 1.0 }; self.cmd_out.send(CmdVel { linear: speed, angular: 0.0 }); } } // SAFETY: stop sending velocity commands on shutdown fn shutdown(&mut self) -> Result<()> { self.cmd_out.send(CmdVel { linear: 0.0, angular: 0.0 }); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(LidarProcessor::new()) // NOTE: .rate() auto-enables RT scheduling with 80% budget and 95% deadline .rate(50.hz()) .build()?; scheduler.run() } ``` --- ## Introspection & Diagnostics | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&str` | Topic name | | `has_message()` | `bool` | At least one message available | | `pending_count()` | `u64` | Messages waiting to be consumed | | `dropped_count()` | `u64` | Messages dropped (buffer overflow) | | `metrics()` | `TopicMetrics` | Aggregated send/recv/failure counts | ```rust // Monitor topic health let m = topic.metrics(); if topic.dropped_count() > 0 { hlog!(warn, "Topic '{}': {} messages dropped, {} pending", topic.name(), topic.dropped_count(), topic.pending_count()); } ``` --- ## Real-World Scenarios ### Emergency Stop Chain Use `send_blocking()` for critical commands that must never be dropped: ```rust struct SafetyMonitor { estop_pub: Topic, } impl Node for SafetyMonitor { fn name(&self) -> &str { "SafetyMonitor" } fn tick(&mut self) { if self.detect_collision() { let stop = EmergencyStop { engaged: 1, reason: *b"Collision detected\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", ..Default::default() }; // Block up to 1ms — guaranteed delivery for safety-critical commands let _ = self.estop_pub.send_blocking(stop, 1.ms()); } } } ``` ### Multi-Sensor Fusion Multiple subscribers reading from different sensors, fusing into a single output: ```rust struct FusionNode { imu_sub: Topic, odom_sub: Topic, pose_pub: Topic, last_imu: Option, last_odom: Option, } impl Node for FusionNode { fn name(&self) -> &str { "Fusion" } fn tick(&mut self) { // Always drain both topics every tick if let Some(imu) = self.imu_sub.recv() { self.last_imu = Some(imu); } if let Some(odom) = self.odom_sub.recv() { self.last_odom = Some(odom); } if let (Some(imu), Some(odom)) = (&self.last_imu, &self.last_odom) { let fused = self.fuse(imu, odom); self.pose_pub.send(fused); } } } ``` --- ## See Also - [Topic Concepts](/concepts/core-concepts-topic) — Architecture, backends, and design patterns - [Communication Overview](/concepts/communication-overview) — When to use topics vs services vs actions - [Image API](/rust/api/image) — Pool-backed camera images - [PointCloud API](/rust/api/pointcloud) — Pool-backed 3D point clouds - [DepthImage API](/rust/api/depth-image) — Pool-backed depth images - [Tensor Messages](/rust/api/tensor-messages) — Tensor, Device, TensorDtype, and domain types - [Scheduler API](/rust/api/scheduler) — Running nodes that use topics - [Python Topic API](/python/api/python-bindings) — Python topic bindings --- ## Services API Path: /rust/api/services Description: Synchronous request/response RPC between HORUS nodes # Services API HORUS services provide synchronous request/response communication between nodes. Define a service with the `service!` macro, run a server with `ServiceServerBuilder`, and call it with `ServiceClient` or `AsyncServiceClient`. ## Defining a Service Use the `service!` macro to define request and response types: ```rust use horus::prelude::*; service! { /// Look up a robot's current pose by name. GetRobotPose { request { robot_name: String, } response { x: f64, y: f64, theta: f64, timestamp_ns: u64, } } } ``` --- ## Service Trait All services implement the `Service` trait: | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&'static str` | Service name (used as topic prefix) | | `request_topic()` | `String` | Request channel name (`"{name}.request"`) | | `response_topic()` | `String` | Response channel name (`"{name}.response"`) | | `request_type_name()` | `&'static str` | Human-readable request type name | | `response_type_name()` | `&'static str` | Human-readable response type name | --- ## ServiceClient (Blocking) Synchronous client that blocks until a response arrives or the timeout elapses. ### Constructor | Method | Returns | Description | |--------|---------|-------------| | `ServiceClient::::new()` | `Result` | Create a client with default 1ms poll interval | | `ServiceClient::::with_poll_interval(interval)` | `Result` | Create a client with custom poll interval | ### Calling | Method | Returns | Description | |--------|---------|-------------| | `call(request, timeout)` | `ServiceResult` | Block until response or timeout | | `call_resilient(request, timeout)` | `ServiceResult` | Auto-retry on transient errors (3 retries, 10ms backoff, 2x multiplier) | | `call_resilient_with(request, timeout, config)` | `ServiceResult` | Auto-retry with custom `RetryConfig` | | `call_optional(request, timeout)` | `ServiceResult>` | Returns `Ok(None)` on timeout instead of `Err` | Only transient errors (`Timeout`, `Transport`) are retried. Permanent errors (`ServiceFailed`, `NoServer`) propagate immediately. ### Detailed Method Reference #### call ```rust pub fn call(&self, request: S::Request, timeout: Duration) -> ServiceResult ``` Send a request and block until a response arrives or the timeout elapses. **Parameters:** - `request: S::Request` — The typed request message. `S` is the service type implementing `ServiceDef`. - `timeout: Duration` — Maximum time to wait for a response. Use `100_u64.ms()`, `1_u64.secs()`, etc. **Returns:** `ServiceResult` — `Ok(response)` or `Err(ServiceError)`. **Errors:** - `ServiceError::Timeout` — No response within timeout - `ServiceError::NoServer` — No server is registered for this service - `ServiceError::ServiceFailed(msg)` — Server returned an error **Example:** ```rust let response = client.call(GetPose { frame: "base" }, 100.ms())?; println!("x={}, y={}", response.x, response.y); ``` #### call_resilient ```rust pub fn call_resilient(&self, request: S::Request, timeout: Duration) -> ServiceResult ``` Like `call()`, but auto-retries on transient errors (timeout, transport). Uses default retry config: 3 retries, 10ms backoff, 2x multiplier. **Retry behavior:** Only `Timeout` and `Transport` errors trigger retries. `NoServer` and `ServiceFailed` propagate immediately. Total time can exceed `timeout` due to retries. #### call_optional ```rust pub fn call_optional(&self, request: S::Request, timeout: Duration) -> ServiceResult> ``` Like `call()`, but returns `Ok(None)` on timeout instead of `Err(Timeout)`. Useful for non-critical lookups where missing data is acceptable. **Returns:** `Ok(Some(response))` on success, `Ok(None)` on timeout, `Err(e)` on other errors. ### Example ```rust use horus::prelude::*; // Create client for the GetRobotPose service let mut client = ServiceClient::::new()?; // Blocking call with 1-second timeout let response = client.call( GetRobotPoseRequest { robot_name: "arm_1".into() }, 1_u64.secs(), )?; println!("Robot at ({:.2}, {:.2})", response.x, response.y); // Resilient call — retries on transient failures let response = client.call_resilient( GetRobotPoseRequest { robot_name: "arm_1".into() }, 2_u64.secs(), )?; // Optional call — Ok(None) on timeout match client.call_optional( GetRobotPoseRequest { robot_name: "arm_1".into() }, 100_u64.ms(), )? { Some(res) => println!("Pose: ({:.2}, {:.2})", res.x, res.y), None => println!("Pose server not responding"), } ``` --- ## AsyncServiceClient (Non-Blocking) Non-blocking client that returns a `PendingServiceCall` handle. Check the handle each tick without blocking the scheduler. ### Constructor | Method | Returns | Description | |--------|---------|-------------| | `AsyncServiceClient::::new()` | `Result` | Create with default 1ms poll interval | | `AsyncServiceClient::::with_poll_interval(interval)` | `Result` | Create with custom poll interval | ### Calling | Method | Returns | Description | |--------|---------|-------------| | `call_async(request, timeout)` | `PendingServiceCall` | Send request, return pending handle immediately | ### PendingServiceCall | Method | Returns | Description | |--------|---------|-------------| | `check()` | `ServiceResult>` | Non-blocking check: `Ok(Some(res))` if ready, `Ok(None)` if waiting, `Err` on timeout/failure | | `wait()` | `ServiceResult` | Block until response arrives or timeout | | `is_expired()` | `bool` | Whether the deadline has passed | ### Example ```rust use horus::prelude::*; service! { GetRobotPose { request { robot_name: String } response { x: f64, y: f64, theta: f64, timestamp_ns: u64 } } } struct PlannerNode { client: AsyncServiceClient, pending: Option>, } impl Node for PlannerNode { fn name(&self) -> &str { "Planner" } fn tick(&mut self) { // Send request if none pending if self.pending.is_none() { self.pending = Some(self.client.call_async( GetRobotPoseRequest { robot_name: "arm_0".into() }, 500_u64.ms(), )); } // Check for response (non-blocking) if let Some(ref mut call) = self.pending { match call.check() { Ok(Some(pose)) => { hlog!(info, "Robot at ({:.2}, {:.2})", pose.x, pose.y); self.pending = None; } Ok(None) => {} // Still waiting Err(e) => { hlog!(warn, "Service call failed: {}", e); self.pending = None; } } } } } ``` --- ## ServiceServerBuilder Fluent builder for creating a service server. | Method | Returns | Description | |--------|---------|-------------| | `ServiceServerBuilder::::new()` | `Self` | Create a new builder | | `on_request(handler)` | `Self` | Register the request handler (`Fn(Req) -> Result`) | | `poll_interval(interval)` | `Self` | Override poll interval (default: 5ms) | | `build()` | `Result>` | Build and start the server (spawns background thread) | The handler receives the request payload and returns either a response (`Ok`) or an error message (`Err`). ### ServiceServer | Method | Returns | Description | |--------|---------|-------------| | `stop()` | `()` | Stop the server (also happens automatically on drop) | The server runs in a background thread. Dropping the `ServiceServer` handle shuts it down. ### Example ```rust use horus::prelude::*; use std::collections::HashMap; // Build and start server let poses: HashMap = HashMap::from([ ("arm_1".into(), (1.5, 2.0, 0.0)), ("arm_2".into(), (3.0, 1.0, 1.57)), ]); let server = ServiceServerBuilder::::new() .on_request(move |req| { match poses.get(&req.robot_name) { Some(&(x, y, theta)) => Ok(GetRobotPoseResponse { x, y, theta, timestamp_ns: horus::timestamp_now(), }), None => Err(format!("Unknown robot: {}", req.robot_name)), } }) .poll_interval(1_u64.ms()) .build()?; // Server runs in background thread until dropped ``` --- ## ServiceRequest / ServiceResponse Wrapper types that flow over the wire: ### ServiceRequest\ | Field | Type | Description | |-------|------|-------------| | `request_id` | `u64` | Unique correlation ID (auto-assigned by client) | | `payload` | `Req` | The actual request data | ### ServiceResponse\ | Field | Type | Description | |-------|------|-------------| | `request_id` | `u64` | Echoes the request's correlation ID | | `ok` | `bool` | `true` if handled successfully | | `payload` | `Option` | Response data (`Some` when `ok == true`) | | `error` | `Option` | Error message (`Some` when `ok == false`) | | Method | Returns | Description | |--------|---------|-------------| | `ServiceResponse::success(request_id, payload)` | `Self` | Create a successful response | | `ServiceResponse::failure(request_id, error)` | `Self` | Create an error response | --- ## ServiceError | Variant | Description | Transient? | |---------|-------------|------------| | `Timeout` | Call timed out waiting for response | Yes | | `ServiceFailed(String)` | Server returned an error | No | | `NoServer` | No server registered for this service | No | | `Transport(String)` | Topic I/O error | Yes | | Method | Returns | Description | |--------|---------|-------------| | `is_transient()` | `bool` | Whether a retry may succeed (`Timeout` and `Transport` are transient) | --- ## ServiceInfo Metadata returned by `horus service list`: | Field | Type | Description | |-------|------|-------------| | `name` | `String` | Service name | | `request_type` | `String` | Rust type name of request | | `response_type` | `String` | Rust type name of response | | `servers` | `usize` | Active server count (typically 0 or 1) | | `clients` | `usize` | Known client count | --- ## Complete Example ```rust use horus::prelude::*; use std::collections::HashMap; use std::sync::{Arc, Mutex}; // Define a key-value store service service! { /// Get or set values in a shared store. KeyValueStore { request { key: String, value: Option, // None = get, Some = set } response { value: Option, found: bool, } } } fn main() -> Result<()> { let store = Arc::new(Mutex::new(HashMap::::new())); let store_clone = store.clone(); // Start server let _server = ServiceServerBuilder::::new() .on_request(move |req| { let mut map = store_clone.lock().unwrap(); match req.value { Some(val) => { map.insert(req.key, val.clone()); Ok(KeyValueStoreResponse { value: Some(val), found: true }) } None => { let val = map.get(&req.key).cloned(); let found = val.is_some(); Ok(KeyValueStoreResponse { value: val, found }) } } }) .build()?; // Client: set a value let mut client = ServiceClient::::new()?; client.call( KeyValueStoreRequest { key: "robot_id".into(), value: Some("arm_01".into()) }, 1_u64.secs(), )?; // Client: get it back let res = client.call( KeyValueStoreRequest { key: "robot_id".into(), value: None }, 1_u64.secs(), )?; assert_eq!(res.value, Some("arm_01".into())); assert!(res.found); Ok(()) } ``` --- ## See Also - [Services Concepts](/concepts/services) — Architecture and design patterns - [Actions API](/rust/api/actions) — Long-running tasks with feedback and cancellation - [Topic API](/rust/api/topic) — Streaming pub/sub communication - [Error Handling](/development/error-handling) — RetryConfig for resilient calls --- ## Time API Path: /rust/time-api Description: Framework clock, timestep, and RNG — horus::now(), horus::dt(), horus::rng(), and more # Time API The `horus::` time functions are THE standard way to get time, timestep, and random numbers in HORUS nodes — same pattern as `hlog!()` for logging. The scheduler sets the ambient context before each `tick()` call; these functions read from it. ## Quick Reference | Function | Returns | Normal Mode | Deterministic Mode | |----------|---------|-------------|-------------------| | `horus::now()` | `TimeStamp` | Wall clock | Virtual SimClock | | `horus::since(ts)` | `Duration` | Elapsed since `ts` | Elapsed (virtual) | | `horus::dt()` | `Duration` | Real elapsed since last tick | Fixed `1/rate` | | `horus::elapsed()` | `Duration` | Wall time since scheduler start | Accumulated virtual time | | `horus::tick()` | `u64` | Current tick number | Current tick number | | `horus::rng(f)` | `R` | System entropy | Tick-seeded (deterministic) | | `horus::budget_remaining()` | `Duration` | Time left in budget | Time left (SimClock) | All functions are safe to call outside `tick()` — they return sensible fallback values. ## `horus::now()` — Current Time Returns a `TimeStamp` representing the current framework time. ```rust fn tick(&mut self) { let start = horus::now(); self.do_expensive_work(); let elapsed = horus::since(start); hlog!(debug, "work took {:?}", elapsed); } ``` ### `TimeStamp` Type `TimeStamp` is an opaque wrapper with nanosecond precision: ```rust let a = horus::now(); let b = horus::now(); // Subtraction produces Duration let diff: Duration = b - a; // Elapsed since a timestamp let elapsed: Duration = a.elapsed(); // Comparison assert!(b > a); // Display println!("{}", a); // "1.500000s" // Serialization let nanos: u64 = a.as_nanos(); let restored = TimeStamp::from_nanos(nanos); ``` ## `horus::dt()` — Timestep Returns the timestep for the current tick. Use this for physics integration: ```rust fn tick(&mut self) { let dt = horus::dt(); self.position += self.velocity * dt.as_secs_f64(); self.velocity += self.acceleration * dt.as_secs_f64(); } ``` In normal mode, `dt()` returns the actual elapsed time since the last tick. In deterministic mode, it returns a fixed value of `1/rate` (e.g., 10ms for 100Hz, 1ms for 1kHz). ## `horus::elapsed()` — Time Since Start Total time since the scheduler started: ```rust fn tick(&mut self) { if horus::elapsed() > Duration::from_secs(30) { hlog!(info, "Running for 30 seconds, switching to cruise mode"); self.mode = Mode::Cruise; } } ``` ## `horus::tick()` — Tick Number Zero-based tick counter: ```rust fn tick(&mut self) { if horus::tick() % 100 == 0 { hlog!(info, "Checkpoint at tick {}", horus::tick()); } } ``` ## `horus::rng()` — Deterministic Random Numbers Returns random values via a closure. In deterministic mode, the RNG is seeded from the tick number and node name — same sequence every run. ```rust fn tick(&mut self) { // Random float in range let noise: f64 = horus::rng(|r| { use rand::Rng; r.gen_range(-0.01..0.01) }); self.measurement += noise; // Random bool let should_explore: bool = horus::rng(|r| { use rand::Rng; r.gen_bool(0.1) // 10% chance }); // Random integer let index: usize = horus::rng(|r| { use rand::Rng; r.gen_range(0..self.candidates.len()) }); } ``` ## `horus::budget_remaining()` — Anytime Algorithms Returns the time remaining in the current tick's budget. Use this for anytime algorithms that improve their result until time runs out: ```rust fn tick(&mut self) { let mut plan = self.current_plan.clone(); loop { plan = self.improve_plan(plan); if horus::budget_remaining() < 50_u64.us() { break; // stop before budget runs out } } self.path_topic.send(plan); } ``` Returns `Duration::MAX` if no budget is configured or outside `tick()`. ## Fallback Behavior All functions are safe to call outside `tick()`: | Function | Outside `tick()` | |----------|-----------------| | `horus::now()` | Wall clock (fallback) | | `horus::dt()` | `Duration::ZERO` | | `horus::elapsed()` | `Duration::ZERO` | | `horus::tick()` | `0` | | `horus::rng(f)` | System entropy (non-deterministic) | | `horus::budget_remaining()` | `Duration::MAX` | ## Python API The time functions are available as module-level functions in Python: ```python import horus # In a node's tick(): now = horus.time_now() # float (seconds) dt = horus.time_dt() # float (seconds) elapsed = horus.time_elapsed() # float (seconds) tick = horus.time_tick() # int budget = horus.time_budget_remaining() # float (seconds, inf if no budget) rng_val = horus.time_rng_float() # float in [0, 1) ``` --- ## Actions API Path: /rust/api/actions Description: Long-running tasks with feedback, cancellation, and priority-based preemption # Actions API HORUS actions model long-running tasks with real-time feedback and cancellation. Define an action with the `action!` macro, build a server with `ActionServerBuilder`, and send goals with `ActionClientNode` or `SyncActionClient`. ## Defining an Action ```rust use horus::prelude::*; action! { /// Navigate to a target pose. NavigateToPose { goal { x: f64, y: f64, theta: f64, } feedback { distance_remaining: f64, estimated_time_sec: f64, } result { success: bool, final_x: f64, final_y: f64, } } } ``` This generates four types: - `NavigateToPoseGoal` — goal struct - `NavigateToPoseFeedback` — feedback struct - `NavigateToPoseResult` — result struct - `NavigateToPose` — zero-sized marker implementing the `Action` trait --- ## Action Trait | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&'static str` | Action name (used as topic prefix) | | `goal_topic()` | `String` | `"{name}.goal"` | | `cancel_topic()` | `String` | `"{name}.cancel"` | | `result_topic()` | `String` | `"{name}.result"` | | `feedback_topic()` | `String` | `"{name}.feedback"` | | `status_topic()` | `String` | `"{name}.status"` | --- ## GoalStatus Lifecycle of a goal: | Variant | Terminal? | Description | |---------|-----------|-------------| | `Pending` | No | Received but not yet processed | | `Active` | No | Currently executing | | `Succeeded` | Yes | Completed successfully | | `Aborted` | Yes | Failed due to server error | | `Canceled` | Yes | Canceled by client request | | `Preempted` | Yes | Preempted by higher-priority goal | | `Rejected` | Yes | Validation failed at acceptance | | Method | Returns | Description | |--------|---------|-------------| | `is_active()` | `bool` | `Pending` or `Active` | | `is_terminal()` | `bool` | Reached a final state | | `is_success()` | `bool` | `Succeeded` | | `is_failure()` | `bool` | `Aborted`, `Canceled`, `Preempted`, or `Rejected` | --- ## GoalPriority | Constant | Value | Description | |----------|-------|-------------| | `HIGHEST` | 0 | Critical goals | | `HIGH` | 64 | Above normal | | `NORMAL` | 128 | Default priority | | `LOW` | 192 | Below normal | | `LOWEST` | 255 | Background tasks | Lower value = higher priority. `is_higher_than(other)` compares priorities. --- ## PreemptionPolicy Controls what happens when a new goal arrives while one is active: | Variant | Description | |---------|-------------| | `RejectNew` | New goals rejected while one is active | | `PreemptOld` | New goals cancel active goals **(default)** | | `Priority` | Higher priority preempts lower priority | | `Queue { max_size }` | Queue goals up to `max_size` | --- ## GoalResponse / CancelResponse Server returns these from goal acceptance and cancel callbacks: | Type | Variants | Methods | |------|----------|---------| | `GoalResponse` | `Accept`, `Reject(String)` | `is_accepted()`, `is_rejected()`, `rejection_reason()` | | `CancelResponse` | `Accept`, `Reject(String)` | `is_accepted()`, `is_rejected()`, `rejection_reason()` | --- ## ActionServerConfig | Field | Type | Default | Description | |-------|------|---------|-------------| | `max_concurrent_goals` | `Option` | `Some(1)` | Max simultaneous goals (`None` = unlimited) | | `feedback_rate_hz` | `f64` | `10.0` | Rate limit for feedback publishing | | `goal_timeout` | `Option` | `None` | Auto-abort after timeout | | `preemption_policy` | `PreemptionPolicy` | `PreemptOld` | How to handle competing goals | | `result_history_size` | `usize` | `100` | How many results to keep in history | Builder methods: `new()`, `unlimited_goals()`, `max_goals(n)`, `feedback_rate(hz)`, `timeout(dur)`, `preemption(policy)`, `history_size(n)`. --- ## ActionError | Variant | Description | |---------|-------------| | `GoalRejected(String)` | Server rejected the goal | | `GoalCanceled` | Goal was canceled | | `GoalPreempted` | Goal was preempted by higher priority | | `GoalTimeout` | Goal timed out | | `ServerUnavailable` | No action server running | | `CommunicationError(String)` | Topic I/O failure | | `ExecutionError(String)` | Server execution error | | `InvalidGoal(String)` | Goal validation failed | | `GoalNotFound(GoalId)` | Goal ID not recognized | --- ## ActionServerBuilder | Method | Returns | Description | |--------|---------|-------------| | `ActionServerBuilder::::new()` | `Self` | Create a new builder | | `on_goal(callback)` | `Self` | Goal acceptance callback (`Fn(&Goal) -> GoalResponse`) | | `on_cancel(callback)` | `Self` | Cancel request callback (`Fn(GoalId) -> CancelResponse`) | | `on_execute(callback)` | `Self` | Execution callback (`Fn(ServerGoalHandle) -> GoalOutcome`) | | `with_config(config)` | `Self` | Apply full `ActionServerConfig` | | `max_concurrent_goals(max)` | `Self` | Shorthand for max concurrent goals | | `feedback_rate(rate_hz)` | `Self` | Shorthand for feedback rate | | `goal_timeout(timeout)` | `Self` | Shorthand for goal timeout | | `preemption_policy(policy)` | `Self` | Shorthand for preemption policy | | `build()` | `ActionServerNode` | Build the server node | ### ActionServerNode Implements `Node` — add it to a `Scheduler` to process goals. | Method | Returns | Description | |--------|---------|-------------| | `builder()` | `ActionServerBuilder` | Create a new builder | | `metrics()` | `ActionServerMetrics` | Get server metrics snapshot | ### ActionServerMetrics | Field | Type | Description | |-------|------|-------------| | `goals_received` | `u64` | Total goals received | | `goals_accepted` | `u64` | Goals that passed acceptance | | `goals_rejected` | `u64` | Goals rejected by `on_goal` | | `goals_succeeded` | `u64` | Successfully completed | | `goals_aborted` | `u64` | Aborted by server | | `goals_canceled` | `u64` | Canceled by client | | `goals_preempted` | `u64` | Preempted by higher priority | | `active_goals` | `usize` | Currently executing | | `queued_goals` | `usize` | Waiting in queue | --- ## ServerGoalHandle Handle passed to the `on_execute` callback. Use it to publish feedback and finalize the goal. ### Query Methods | Method | Returns | Description | |--------|---------|-------------| | `goal_id()` | `GoalId` | Unique goal identifier | | `goal()` | `&A::Goal` | The goal data | | `priority()` | `GoalPriority` | Goal priority level | | `status()` | `GoalStatus` | Current status | | `elapsed()` | `Duration` | Time since execution started | | `is_cancel_requested()` | `bool` | Client requested cancellation | | `is_preempt_requested()` | `bool` | Higher-priority goal wants to preempt | | `should_abort()` | `bool` | `true` if canceled or preempted — check this in loops | ### Action Methods | Method | Returns | Description | |--------|---------|-------------| | `publish_feedback(feedback)` | `()` | Send feedback to client (rate-limited) | | `succeed(result)` | `GoalOutcome` | Complete successfully | | `abort(result)` | `GoalOutcome` | Abort with error | | `canceled(result)` | `GoalOutcome` | Acknowledge cancellation | | `preempted(result)` | `GoalOutcome` | Acknowledge preemption | ### GoalOutcome | Variant | Description | |---------|-------------| | `Succeeded(A::Result)` | Goal completed successfully | | `Aborted(A::Result)` | Server aborted execution | | `Canceled(A::Result)` | Client canceled | | `Preempted(A::Result)` | Preempted by higher priority | Methods: `status()` returns `GoalStatus`, `into_result()` extracts the result. --- ## Server Example ```rust use horus::prelude::*; action! { MoveArm { goal { target_x: f64, target_y: f64, target_z: f64 } feedback { progress: f64, current_x: f64, current_y: f64, current_z: f64 } result { success: bool, final_x: f64, final_y: f64, final_z: f64 } } } let server = ActionServerNode::::builder() .on_goal(|goal| { // Validate the goal if goal.target_z < 0.0 { GoalResponse::Reject("Z must be non-negative".into()) } else { GoalResponse::Accept } }) .on_cancel(|_goal_id| CancelResponse::Accept) .on_execute(|handle| { let goal = handle.goal(); let mut progress = 0.0; while progress < 1.0 { // Check for cancellation if handle.should_abort() { return handle.canceled(MoveArmResult { success: false, final_x: 0.0, final_y: 0.0, final_z: 0.0, }); } progress += 0.01; handle.publish_feedback(MoveArmFeedback { progress, current_x: goal.target_x * progress, current_y: goal.target_y * progress, current_z: goal.target_z * progress, }); std::thread::sleep(10_u64.ms()); } handle.succeed(MoveArmResult { success: true, final_x: goal.target_x, final_y: goal.target_y, final_z: goal.target_z, }) }) .preemption_policy(PreemptionPolicy::Priority) .goal_timeout(30_u64.secs()) .build(); let mut scheduler = Scheduler::new(); scheduler.add(server).order(0).build(); ``` --- ## ActionClientBuilder | Method | Returns | Description | |--------|---------|-------------| | `ActionClientBuilder::::new()` | `Self` | Create a new builder | | `on_feedback(callback)` | `Self` | Feedback callback (`Fn(GoalId, &Feedback)`) | | `on_result(callback)` | `Self` | Result callback (`Fn(GoalId, GoalStatus, &Result)`) | | `on_status(callback)` | `Self` | Status change callback (`Fn(GoalId, GoalStatus)`) | | `build()` | `ActionClientNode` | Build the client node | ### ActionClientNode Implements `Node` — add it to a `Scheduler` alongside the server. | Method | Returns | Description | |--------|---------|-------------| | `builder()` | `ActionClientBuilder` | Create a new builder | | `send_goal(goal)` | `Result` | Send with `NORMAL` priority | | `send_goal_with_priority(goal, priority)` | `Result` | Send with specific priority | | `cancel_goal(goal_id)` | `()` | Request cancellation | | `goal_status(goal_id)` | `Option` | Query goal status | | `active_goals()` | `Vec` | All active goal IDs | | `active_goal_count()` | `usize` | Number of active goals | | `metrics()` | `ActionClientMetrics` | Get client metrics | ### ActionClientMetrics | Field | Type | Description | |-------|------|-------------| | `goals_sent` | `u64` | Total goals sent | | `goals_succeeded` | `u64` | Successfully completed | | `goals_failed` | `u64` | Aborted, canceled, preempted, or rejected | | `cancels_sent` | `u64` | Cancel requests sent | | `active_goals` | `usize` | Currently active | --- ## ClientGoalHandle Handle returned by `send_goal()`. Use it to monitor progress and get results. ### Query Methods | Method | Returns | Description | |--------|---------|-------------| | `goal_id()` | `GoalId` | Unique goal identifier | | `priority()` | `GoalPriority` | Goal priority level | | `status()` | `GoalStatus` | Current status | | `is_active()` | `bool` | Still executing | | `is_done()` | `bool` | Reached terminal state | | `is_success()` | `bool` | Completed successfully | | `elapsed()` | `Duration` | Time since goal was sent | | `time_since_update()` | `Duration` | Time since last status change | | `result()` | `Option` | Get result if completed | | `last_feedback()` | `Option` | Most recent feedback | ### Blocking Methods | Method | Returns | Description | |--------|---------|-------------| | `await_result(timeout)` | `Option` | Block until result or timeout | | `await_result_with_feedback(timeout, callback)` | `Result` | Block with feedback callback | | `cancel()` | `()` | Send cancel request to server | --- ## SyncActionClient (Blocking) Standalone blocking client — does not need a `Scheduler`. | Method | Returns | Description | |--------|---------|-------------| | `SyncActionClient::::new()` | `Result` | Create and initialize | | `send_goal_and_wait(goal, timeout)` | `Result` | Send goal, block until result | | `send_goal_and_wait_with_feedback(goal, timeout, callback)` | `Result` | Block with feedback | | `cancel_goal(goal_id)` | `()` | Request cancellation | Type alias: `ActionClient = SyncActionClient` ### Example ```rust use horus::prelude::*; // Simple blocking usage (no scheduler needed) let client = SyncActionClient::::new()?; let result = client.send_goal_and_wait_with_feedback( MoveArmGoal { target_x: 1.0, target_y: 2.0, target_z: 0.5 }, 30_u64.secs(), |feedback| { println!("Progress: {:.0}%", feedback.progress * 100.0); }, )?; if result.success { println!("Arm reached ({:.1}, {:.1}, {:.1})", result.final_x, result.final_y, result.final_z); } ``` --- ## GoalId Unique identifier for each goal (`Uuid`-backed). | Method | Returns | Description | |--------|---------|-------------| | `GoalId::new()` | `Self` | Generate a new unique ID | | `GoalId::from_uuid(uuid)` | `Self` | Create from existing UUID | | `as_uuid()` | `&Uuid` | Get underlying UUID | --- ## See Also - [Actions Concepts](/concepts/actions) — Architecture and lifecycle design - [Services API](/rust/api/services) — Synchronous request/response RPC - [Topic API](/rust/api/topic) — Streaming pub/sub communication - [Scheduler API](/rust/api/scheduler) — Node execution orchestrator --- ## Rate & Stopwatch Path: /rust/api/rate-stopwatch Description: Fixed-frequency rate limiting and elapsed time measurement for background threads and performance profiling # Rate & Stopwatch Two timing utilities exported from `horus::prelude::*` for use outside the scheduler's tick loop. ```rust use horus::prelude::*; ``` ## Rate — Fixed-Frequency Loop `Rate` is the HORUS equivalent of ROS2's `rclcpp::Rate`. Use it for standalone threads that need to run at a target frequency without a scheduler. ```rust use horus::prelude::*; // Hardware polling thread at 100 Hz std::thread::spawn(|| { let mut rate = Rate::new(100.0); loop { let reading = read_sensor(); process(reading); rate.sleep(); // Sleeps the remaining fraction of 10ms } }); ``` ### How It Works `rate.sleep()` calculates how much time remains in the current period and sleeps for that duration. If work took longer than the period, sleep is skipped and the next cycle catches up — no drift accumulation. ### API | Method | Returns | Description | |---|---|---| | `Rate::new(hz)` | `Rate` | Create a rate limiter at `hz` Hz. Panics if `hz <= 0` | | `.sleep()` | `()` | Sleep for the remainder of the current period | | `.actual_hz()` | `f64` | Exponentially smoothed actual frequency | | `.target_hz()` | `f64` | The target frequency in Hz | | `.period()` | `Duration` | The target period (1/hz) | | `.reset()` | `()` | Reset cycle start to now (use after a long pause) | | `.is_late()` | `bool` | Whether the current cycle exceeded the target period | ### Example: Hardware Driver Thread ```rust use horus::prelude::*; struct CanBusReader { topic: Topic, } impl CanBusReader { fn run(&mut self) { let mut rate = Rate::new(500.0); // 500 Hz CAN bus polling loop { if let Some(frame) = self.read_can_frame() { let cmd = MotorCommand::from_can(frame); self.topic.send(cmd); } if rate.is_late() { hlog!(warn, "CAN polling late — actual {:.0} Hz", rate.actual_hz()); } rate.sleep(); } } } ``` ### When to Use Rate vs Scheduler | Scenario | Use | |---|---| | Node with `tick()` in a scheduler | Scheduler handles timing — don't use Rate | | Background thread polling hardware | `Rate` | | Standalone process (no scheduler) | `Rate` | | One-off timed loop in a test | `Rate` | ## Stopwatch — Elapsed Time `Stopwatch` measures elapsed time with lap support. Useful for profiling operations inside nodes. ```rust use horus::prelude::*; let mut sw = Stopwatch::start(); expensive_computation(); hlog!(debug, "computation took {:.2} ms", sw.elapsed_ms()); ``` ### API | Method | Returns | Description | |---|---|---| | `Stopwatch::start()` | `Stopwatch` | Create and start immediately | | `.elapsed()` | `Duration` | Time since start (or last reset) | | `.elapsed_us()` | `u64` | Elapsed microseconds | | `.elapsed_ms()` | `f64` | Elapsed milliseconds (fractional) | | `.lap()` | `Duration` | Return elapsed and reset (for split timing) | | `.reset()` | `()` | Reset start time to now | ### Example: Profiling a Node ```rust use horus::prelude::*; struct PlannnerNode { scan_sub: Topic, path_pub: Topic, } impl Node for PlannnerNode { fn name(&self) -> &str { "Planner" } fn tick(&mut self) { if let Some(scan) = self.scan_sub.recv() { let mut sw = Stopwatch::start(); let path = self.compute_path(&scan); let plan_time = sw.lap(); self.path_pub.send(path); let total_time = sw.elapsed(); hlog!(debug, "plan={:.1}ms total={:.1}ms", plan_time.as_secs_f64() * 1000.0, (plan_time + total_time).as_secs_f64() * 1000.0); } } } ``` ### Example: Multi-Lap Benchmarking ```rust let mut sw = Stopwatch::start(); let data = load_model(); let load_time = sw.lap(); let result = run_inference(&data); let infer_time = sw.lap(); save_result(&result); let save_time = sw.lap(); hlog!(info, "load={:.1}ms infer={:.1}ms save={:.1}ms", load_time.as_secs_f64() * 1000.0, infer_time.as_secs_f64() * 1000.0, save_time.as_secs_f64() * 1000.0); ``` ## See Also - **[Time API](/rust/time-api)** — `horus::now()`, `horus::dt()`, `horus::elapsed()` for scheduler-aware time - **[DurationExt](/rust/api/duration-ext)** — `100_u64.hz()`, `200_u64.us()` ergonomic helpers - **[Scheduler](/rust/api/scheduler)** — built-in rate control via `.rate()` and `.tick_rate()` --- ## Error Types Path: /rust/api/error-types Description: Structured error handling — HorusError variants, pattern matching, severity, and retry utilities # Error Types Every error in HORUS is structured and pattern-matchable. Import the short aliases from the prelude: ```rust use horus::prelude::*; // Result = std::result::Result // Error = HorusError fn my_function() -> Result<()> { let topic: Topic = Topic::new("sensor")?; Ok(()) } ``` ## Quick Reference | Prelude Name | Full Name | What it is | |---|---|---| | `Result` | `HorusResult` | `std::result::Result` | | `Error` | `HorusError` | The umbrella error enum | **Always use `Result` and `Error`** — never write `HorusResult` or `HorusError` directly. ## HorusError Variants `HorusError` is a `#[non_exhaustive]` enum with 13 variants. Each wraps a domain-specific sub-error: | Variant | Sub-error Type | Domain | Common Source | |---|---|---|---| | `Io(…)` | `std::io::Error` | File/system I/O | `std::fs::read()` | | `Config(…)` | `ConfigError` | Configuration | `horus.toml` parsing | | `Communication(…)` | `CommunicationError` | IPC, topics | `Topic::new()` | | `Node(…)` | `NodeError` | Node lifecycle | `init()`, `tick()` panics | | `Memory(…)` | `MemoryError` | SHM, tensors | `Image::new()`, pool exhaustion | | `Serialization(…)` | `SerializationError` | JSON, YAML, TOML | Config/message parsing | | `NotFound(…)` | `NotFoundError` | Missing resources | Frame/topic/node lookup | | `Resource(…)` | `ResourceError` | Resource lifecycle | Duplicate names, permissions | | `InvalidInput(…)` | `ValidationError` | Input validation | Parameter bounds, format | | `Parse(…)` | `ParseError` | Type parsing | Integer, float, bool from strings | | `InvalidDescriptor(…)` | `String` | Tensor integrity | Cross-process tensor validation | | `Transform(…)` | `TransformError` | Coordinate frames | Extrapolation, stale data | | `Timeout(…)` | `TimeoutError` | Timeouts | Service calls, resource waits | ## Pattern Matching Match on specific error variants to handle them differently: ```rust use horus::prelude::*; fn handle_topic_error(err: Error) { match err { HorusError::Communication(CommunicationError::TopicFull { topic }) => { // Back-pressure: subscriber is slower than publisher hlog!(warn, "Topic '{}' full, dropping message", topic); } HorusError::Communication(CommunicationError::TopicNotFound { topic }) => { // Topic doesn't exist yet hlog!(error, "Topic '{}' not found — is the publisher running?", topic); } HorusError::Memory(MemoryError::PoolExhausted { reason }) => { // Image/PointCloud pool has no free slots hlog!(warn, "Pool exhausted: {} — waiting for consumers", reason); } HorusError::Transform(TransformError::Stale { frame, age, threshold }) => { // Transform data is too old hlog!(warn, "Frame '{}' stale: {:?} > {:?}", frame, age, threshold); } other => { hlog!(error, "Unexpected error: {}", other); // Check for actionable hints if let Some(hint) = other.help() { hlog!(info, " hint: {}", hint); } } } } ``` ## Error Hints Every `HorusError` has a `.help()` method that returns an actionable remediation hint: ```rust if let Err(e) = Topic::::new("sensor") { eprintln!("error: {}", e); if let Some(hint) = e.help() { eprintln!(" hint: {}", hint); } } ``` Example output: ``` error: Failed to create topic 'sensor': permission denied hint: Check shared memory permissions and available space. Run: horus clean --shm ``` ## Severity Every error has a `Severity` classification used by the scheduler for automatic recovery: | Severity | Meaning | Scheduler Action | |---|---|---| | `Transient` | May resolve on retry (back-pressure, timeout) | Retry | | `Permanent` | Won't succeed but system can continue | Skip, log warning | | `Fatal` | Data integrity compromised, unrecoverable | Stop node or scheduler | ```rust match err.severity() { Severity::Transient => { /* retry */ } Severity::Permanent => { hlog!(warn, "{}", err); } Severity::Fatal => { /* emergency stop */ } } ``` ## Adding Context Use the `HorusContext` trait to wrap foreign errors with descriptive context: ```rust use horus::prelude::*; fn load_sensor_config(path: &str) -> Result { // .horus_context() wraps std::io::Error with a message let data = std::fs::read_to_string(path) .horus_context(format!("reading sensor config '{}'", path))?; Ok(data) } fn load_calibration(path: &str) -> Result { // .horus_context_with() is lazy — closure only called on Err std::fs::read_to_string(path) .horus_context_with(|| format!("reading calibration '{}'", path)) } ``` The context is preserved in the error chain and displayed as: ``` reading sensor config 'sensors.yaml' Caused by: No such file or directory (os error 2) ``` ## Retry Utility `retry_transient` automatically retries operations that return transient errors: ```rust use horus::prelude::*; let config = RetryConfig { max_retries: 3, base_delay: 100_u64.ms(), ..Default::default() }; let result = retry_transient(&config, || { connect_to_sensor() }); ``` ## Common Error Scenarios | You're doing | Error you'll see | What to do | |---|---|---| | `Topic::new("name")` | `CommunicationError::TopicCreationFailed` | Check SHM permissions, disk space | | `scheduler.run()` | `NodeError::InitPanic` | Fix the node's `init()` method | | `Image::new(w, h, enc)` | `MemoryError::PoolExhausted` | Consumers aren't dropping images fast enough | | `tf.tf("a", "b")` | `TransformError::Extrapolation` | Increase history buffer or check timestamp | | `client.call(req, timeout)` | `TimeoutError` | Server not running or overloaded | | `params.set("key", val)` | `ValidationError::OutOfRange` | Value outside configured min/max | ## Sub-Error Details ### CommunicationError ```rust TopicFull { topic } // Ring buffer full TopicNotFound { topic } // No such topic TopicCreationFailed { topic, reason } // SHM setup failed NetworkFault { peer, reason } // Peer unreachable ActionFailed { reason } // Action system error ``` ### NotFoundError ```rust Frame { name } // TransformFrame lookup Topic { name } // Topic lookup Node { name } // Node lookup Service { name } // Service lookup Action { name } // Action lookup Parameter { name } // RuntimeParams lookup ``` ### ValidationError ```rust OutOfRange { field, min, max, actual } // Value outside bounds InvalidFormat { field, expected_format, actual } // Wrong format InvalidEnum { field, valid_options, actual } // Not an allowed value MissingRequired { field } // Required field absent ConstraintViolation { field, constraint } // Custom constraint Conflict { field_a, field_b, reason } // Two fields conflict ``` ### ConfigError ```rust ParseFailed { format, reason, source } // Failed to parse config file MissingField { field, context } // Required field missing ValidationFailed { field, expected, actual } // Value doesn't match constraint InvalidValue { key, reason } // Invalid configuration value Other(String) // General config error ``` ### SerializationError ```rust Json { source: serde_json::Error } // JSON parse/emit failure Yaml { source: serde_yaml::Error } // YAML parse/emit failure Toml { source: toml::ser::Error } // TOML parse/emit failure Other { format, reason } // Other format error ``` ### MemoryError ```rust PoolExhausted { reason } // Tensor pool out of slots AllocationFailed { reason } // Memory allocation failed ShmCreateFailed { path, reason } // Shared memory region creation failed MmapFailed { reason } // Memory mapping failed DLPackImportFailed { reason } // DLPack tensor import failed OffsetOverflow // Tensor offset exceeds region ``` ### NodeError ```rust InitPanic { node } // Node panicked during init() ReInitPanic { node } // Node panicked during re-init ShutdownPanic { node } // Node panicked during shutdown() InitFailed { node, reason } // init() returned Err TickFailed { node, reason } // Tick error Other { node, message } // General node error ``` ### ResourceError ```rust AlreadyExists { resource_type, name } // Resource already registered PermissionDenied { resource, required_permission } // Insufficient permissions Unsupported { feature, reason } // Feature not available on this platform ``` ### ParseError ```rust Int { input, source: ParseIntError } // Integer parsing failed Float { input, source: ParseFloatError } // Float parsing failed Bool { input, source: ParseBoolError } // Boolean parsing failed Custom { type_name, input, reason } // Custom type parse failed ``` ### TransformError ```rust Extrapolation { frame, requested_ns, oldest_ns, newest_ns } // Out of buffer range Stale { frame, age, threshold } // Transform too old ``` ### TimeoutError ```rust TimeoutError { resource, elapsed, deadline } // Operation timed out ``` ### Internal & Contextual Variants Two special variants for framework-internal errors: ```rust // Internal error with source location (use horus_internal!() macro) Internal { message: String, file: &'static str, line: u32 } // Error with preserved source chain (use .horus_context() on Results) Contextual { message: String, source: Box } ``` ```rust // Adding context to errors let device = Device::open("/dev/ttyUSB0") .horus_context("opening motor controller serial port")?; ``` ## See Also - **[Error Handling Guide](/development/error-handling)** — high-level patterns and best practices - **[Node Trait](/concepts/core-concepts-nodes)** — lifecycle methods that return `Result` - **[Services](/rust/api/services)** — `ServiceError` for RPC failures - **[Actions](/rust/api/actions)** — `ActionError` for long-running task failures --- ## TransformFrame API Path: /rust/api/transform-frame Description: Lock-free coordinate frame tree with sub-microsecond transform lookups # TransformFrame API `TransformFrame` manages coordinate frame trees for real-time robotics. All lookups are lock-free and wait-free. Register frames with `FrameBuilder`, query transforms with `TransformQuery`, and update dynamic frames at sensor rates. ## Creating a TransformFrame ```rust use horus::prelude::*; // Default: 256 frames, 128 static, 32-entry history let tf = TransformFrame::new(); // Presets let tf = TransformFrame::small(); // 256 frames (~550KB) let tf = TransformFrame::medium(); // 1024 frames (~2.2MB) let tf = TransformFrame::large(); // 4096 frames (~9MB) let tf = TransformFrame::massive(); // 16384 frames (~35MB) // Custom let tf = TransformFrame::with_config( TransformFrameConfig::custom() .max_frames(512) .history_len(64) .enable_overflow(false) // Hard RT: no heap fallback .build()? ); ``` | Constructor | Description | |-------------|-------------| | `TransformFrame::new()` | Default (256 frames, small preset) | | `TransformFrame::small()` | 256 frames, embedded/single robot | | `TransformFrame::medium()` | 1024 frames, complex/multi-robot | | `TransformFrame::large()` | 4096 frames, simulations | | `TransformFrame::massive()` | 16384 frames, 100+ robots | | `TransformFrame::with_config(config)` | Custom configuration | --- ## TransformFrameConfig | Field | Type | Default | Description | |-------|------|---------|-------------| | `max_frames` | `usize` | `256` | Maximum frames (16–65536) | | `max_static_frames` | `usize` | `max_frames/2` | Static frame slots | | `history_len` | `usize` | `32` | History buffer per dynamic frame (4–256) | | `enable_overflow` | `bool` | `true` | Fallback to HashMap if capacity exceeded | | `chain_cache_size` | `usize` | `64` | Cache for transform chain lookups | ### Presets | Preset | Capacity | Static | Memory | Use Case | |--------|----------|--------|--------|----------| | `small()` | 256 | 128 | ~550KB | Single robots, embedded | | `medium()` | 1024 | 512 | ~2.2MB | Complex robots, multi-robot | | `large()` | 4096 | 2048 | ~9MB | Multi-robot simulations | | `massive()` | 16384 | 8192 | ~35MB | Large-scale simulations (100+ robots) | | `unlimited()` | 4096 | 2048 | 35MB+ | Dynamic/unpredictable counts (no RT guarantee) | | `rt_fixed(max_frames)` | custom | max_frames/2 | variable | Hard real-time (no overflow) | ### TransformFrameConfigBuilder ```rust let config = TransformFrameConfig::custom() .max_frames(512) .max_static_frames(256) .history_len(64) .enable_overflow(false) .chain_cache_size(128) .build()?; println!("Memory: {}", config.memory_estimate()); // "1.1 MB" ``` | Method | Returns | Description | |--------|---------|-------------| | `TransformFrameConfig::custom()` | `TransformFrameConfigBuilder` | Start custom config | | `.max_frames(n)` | `Self` | Set max frames | | `.max_static_frames(n)` | `Self` | Set static frame slots | | `.history_len(n)` | `Self` | Set history buffer size | | `.enable_overflow(bool)` | `Self` | Enable/disable heap fallback | | `.chain_cache_size(n)` | `Self` | Set chain cache size | | `.build()` | `Result` | Validate and build | | `config.validate()` | `Result<(), String>` | Check constraints | | `config.estimated_memory_bytes()` | `usize` | Memory estimate in bytes | | `config.memory_estimate()` | `String` | Human-readable memory (e.g., "2.2 MB") | --- ## Registering Frames ### FrameBuilder (Fluent API) The recommended way to register frames: ```rust // Root frame (no parent) tf.add_frame("world").build()?; // Dynamic child frame tf.add_frame("base_link").parent("world").build()?; // Static frame with fixed offset (e.g., sensor mount) tf.add_frame("camera") .parent("base_link") .static_transform(&Transform::xyz(0.1, 0.0, 0.5)) .build()?; ``` | Method | Returns | Description | |--------|---------|-------------| | `tf.add_frame(name)` | `FrameBuilder` | Start registering a frame | | `.parent(name)` | `Self` | Set parent frame | | `.static_transform(transform)` | `Self` | Mark as static with fixed transform (requires parent) | | `.build()` | `Result` | Register and return frame ID | ### Direct Registration ```rust // Dynamic frame let id = tf.register_frame("base_link", Some("world"))?; // Static frame with fixed transform let id = tf.register_static_frame("camera", Some("base_link"), &Transform::xyz(0.1, 0.0, 0.5))?; // Unregister (dynamic only) tf.unregister_frame("temp_frame")?; ``` ### Frame Lookup | Method | Returns | Description | |--------|---------|-------------| | `frame_id(name)` | `Option` | Get frame ID by name (cache this for hot paths) | | `frame_name(id)` | `Option` | Get name by ID | | `has_frame(name)` | `bool` | Check if frame exists | | `all_frames()` | `Vec` | All frame names | | `frame_count()` | `usize` | Total registered frames | | `parent(name)` | `Option` | Parent frame name | | `children(name)` | `Vec` | Direct child frames | --- ## Updating Transforms Update dynamic frames with new transform data at sensor rates: ```rust let now = timestamp_now(); tf.update_transform("base_link", &Transform::xyz(1.0, 2.0, 0.0), now)?; // By ID (fastest, lock-free, no name lookup) let id = tf.frame_id("base_link").unwrap(); tf.update_transform_by_id(id, &Transform::xyz(1.0, 2.0, 0.0), now)?; // Update static frame offset tf.set_static_transform("camera", &Transform::xyz(0.1, 0.0, 0.6))?; ``` | Method | Returns | Description | |--------|---------|-------------| | `update_transform(name, transform, timestamp_ns)` | `Result<()>` | Update by name (validates transform) | | `update_transform_by_id(id, transform, timestamp_ns)` | `Result<()>` | Update by ID (fastest, lock-free) | | `set_static_transform(name, transform)` | `Result<()>` | Update a static frame's offset | Transforms are automatically validated — NaN/Inf values are rejected and quaternions are auto-normalized. --- ## Querying Transforms ### TransformQuery (Fluent API) The recommended way to look up transforms: ```rust // Latest transform from camera to world let t = tf.query("camera").to("world").lookup()?; // At a specific timestamp (with LERP + SLERP interpolation) let t = tf.query("camera").to("world").at(timestamp_ns)?; // Strict: error if extrapolation needed let t = tf.query("camera").to("world").at_strict(timestamp_ns)?; // With tolerance window let t = tf.query("camera").to("world").at_with_tolerance(timestamp_ns, 100_000_000)?; // 100ms // Transform a point let world_pt = tf.query("lidar").to("world").point([1.0, 0.0, 0.0])?; // Transform a vector (rotation only, no translation) let world_vec = tf.query("imu").to("world").vector([0.0, 0.0, 9.81])?; // Check availability if tf.query("sensor").to("world").can_at(timestamp_ns) { let t = tf.query("sensor").to("world").at(timestamp_ns)?; } // Get the frame chain let chain = tf.query("camera").to("world").chain()?; // ["camera", "base_link", "world"] ``` ### TransformQueryFrom | Method | Returns | Description | |--------|---------|-------------| | `tf.query(src)` | `TransformQueryFrom` | Start query from source frame | | `.to(dst)` | `TransformQuery` | Set destination frame | ### TransformQuery Methods | Method | Returns | Description | |--------|---------|-------------| | `lookup()` | `Result` | Latest transform | | `at(timestamp_ns)` | `Result` | At timestamp with interpolation | | `at_strict(timestamp_ns)` | `Result` | Error if extrapolation needed | | `at_with_tolerance(timestamp_ns, tolerance_ns)` | `Result` | Error if gap exceeds tolerance | | `point(xyz)` | `Result<[f64; 3]>` | Transform a 3D point | | `vector(xyz)` | `Result<[f64; 3]>` | Transform a vector (rotation only) | | `can_at(timestamp_ns)` | `bool` | Check if transform available at timestamp | | `can_at_with_tolerance(timestamp_ns, tolerance_ns)` | `bool` | Check within tolerance window | | `chain()` | `Result>` | Frame chain from source to destination | | `wait(timeout)` | `Result` | Block until available (feature `wait`) | | `wait_at(timestamp_ns, timeout)` | `Result` | Block until timestamp data available (feature `wait`) | | `wait_async(timeout)` | `Result` | Async wait (feature `async-wait`) | | `wait_at_async(timestamp_ns, timeout)` | `Result` | Async wait at timestamp (feature `async-wait`) | ### Key Method Details #### lookup ```rust pub fn lookup(&self) -> Result ``` Get the latest known transform between source and destination frames. Returns the most recent transform regardless of timestamp. **Returns:** `Ok(Transform)` with translation (x, y, z) and rotation (quaternion). **Errors:** - `TransformError::FrameNotFound` — Source or destination frame doesn't exist - `TransformError::NoPath` — No chain of frames connects source to destination - `TransformError::EmptyHistory` — Frame exists but no transforms have been published yet #### at ```rust pub fn at(&self, timestamp_ns: u64) -> Result ``` Get the transform at a specific timestamp, with automatic interpolation between stored samples. **Parameters:** - `timestamp_ns: u64` — Timestamp in nanoseconds since epoch. Use `horus::now().as_nanos()`. **Behavior:** If the requested time falls between two stored transforms, SLERP interpolation is used for rotation and linear interpolation for translation. If the time is outside stored history, extrapolation is used (may be inaccurate). **Errors:** Same as `lookup()` plus `TransformError::Stale` if the transform data is too old. #### wait ```rust pub fn wait(&self, timeout: Duration) -> Result ``` Block until the transform becomes available or timeout elapses. Use this when your node starts before the transform publisher. **Parameters:** - `timeout: Duration` — Maximum time to wait. Use `5_u64.secs()`. **Returns:** `Ok(Transform)` once available, `Err(TransformError::Timeout)` if timeout elapses. ### Direct Query Methods For cases where you don't need the fluent builder: | Method | Returns | Description | |--------|---------|-------------| | `tf(src, dst)` | `Result` | Latest transform src→dst | | `tf_by_id(src_id, dst_id)` | `Option` | By ID (fastest, lock-free) | | `tf_at(src, dst, timestamp_ns)` | `Result` | At timestamp with interpolation | | `tf_at_strict(src, dst, timestamp_ns)` | `Result` | Error if extrapolation needed | | `tf_at_with_tolerance(src, dst, ts, tol)` | `Result` | Error if gap exceeds tolerance | | `can_transform(src, dst)` | `bool` | Check if path exists | | `can_transform_at(src, dst, timestamp_ns)` | `bool` | Check at timestamp | | `can_transform_at_with_tolerance(src, dst, ts, tol)` | `bool` | Check within tolerance | | `transform_point(src, dst, point)` | `Result<[f64; 3]>` | One-shot point transform | | `transform_vector(src, dst, vector)` | `Result<[f64; 3]>` | Rotation-only vector transform | > **Argument order**: `tf.tf("camera", "world")` means "transform FROM camera TO world". This is the opposite of ROS2 TF2's `lookupTransform(target, source)`. --- ## Transform A 3D rigid body transform (translation + quaternion rotation) in f64 precision. ### Fields ```rust pub struct Transform { pub translation: [f64; 3], // [x, y, z] in meters pub rotation: [f64; 4], // [x, y, z, w] Hamilton quaternion } ``` ### Constructors ```rust let t = Transform::identity(); let t = Transform::new([1.0, 2.0, 0.0], [0.0, 0.0, 0.0, 1.0]); let t = Transform::xyz(1.0, 2.0, 0.0); let t = Transform::x(1.0); // X-axis only let t = Transform::yaw(1.57); // Z-axis rotation (radians) let t = Transform::rpy(0.0, 0.0, 1.57); // Roll, pitch, yaw let t = Transform::from_euler([1.0, 2.0, 0.0], [0.0, 0.0, 1.57]); let t = Transform::from_translation([1.0, 2.0, 0.0]); let t = Transform::from_rotation([0.0, 0.0, 0.707, 0.707]); let t = Transform::from_matrix(matrix_4x4); // Chainable let t = Transform::xyz(1.0, 0.0, 0.0).with_yaw(1.57); let t = Transform::xyz(1.0, 0.0, 0.5).with_rpy(0.0, 0.1, 0.0); ``` | Constructor | Description | |-------------|-------------| | `identity()` | No translation or rotation | | `new(translation, rotation)` | Custom translation + quaternion (auto-normalized) | | `from_translation(xyz)` | Translation only | | `from_rotation(xyzw)` | Rotation only (auto-normalized) | | `from_euler(translation, rpy)` | Translation + Euler angles (radians) | | `xyz(x, y, z)` | Translation shorthand | | `x(v)` / `y(v)` / `z(v)` | Single-axis translation | | `yaw(angle)` / `pitch(angle)` / `roll(angle)` | Single-axis rotation (radians) | | `rpy(roll, pitch, yaw)` | Combined rotation (radians) | | `from_matrix(m)` | From 4x4 homogeneous matrix (row-major) | | `.with_yaw(angle)` | Compose yaw rotation (chainable) | | `.with_rpy(r, p, y)` | Compose RPY rotation (chainable) | ### Operations | Method | Returns | Description | |--------|---------|-------------| | `compose(other)` | `Transform` | Compose transforms (self * other) | | `inverse()` | `Transform` | Reverse direction | | `transform_point(xyz)` | `[f64; 3]` | Apply rotation + translation to point | | `transform_vector(xyz)` | `[f64; 3]` | Apply rotation only (no translation) | | `interpolate(other, t)` | `Transform` | LERP + SLERP interpolation (t in [0, 1]) | | `to_euler()` | `[f64; 3]` | Convert rotation to [roll, pitch, yaw] radians | | `to_matrix()` | `[[f64; 4]; 4]` | Convert to 4x4 homogeneous matrix | | `validate()` | `Result<(), String>` | Check for NaN/Inf and valid quaternion | | `validated()` | `Result` | Validate and auto-normalize | | `is_identity(epsilon)` | `bool` | Approximate identity test | | `translation_magnitude()` | `f64` | Translation vector length | | `rotation_angle()` | `f64` | Rotation angle in radians | --- ## Staleness Detection Check if frames have gone stale (e.g., sensor disconnected): ```rust // Using wall-clock time if tf.is_stale_now("imu", 500_000_000) { // 500ms hlog!(warn, "IMU data is stale!"); } // Using custom time (for simulation) if tf.is_stale("imu", 500_000_000, sim_time_ns) { hlog!(warn, "IMU stale in sim time"); } // Time since last update if let Some(age_ns) = tf.time_since_last_update_now("imu") { println!("IMU age: {}ms", age_ns / 1_000_000); } ``` | Method | Returns | Description | |--------|---------|-------------| | `is_stale(name, max_age_ns, now_ns)` | `bool` | Stale relative to custom time | | `is_stale_now(name, max_age_ns)` | `bool` | Stale relative to wall-clock | | `time_since_last_update(name, now_ns)` | `Option` | Age in nanoseconds (custom time) | | `time_since_last_update_now(name)` | `Option` | Age in nanoseconds (wall-clock) | | `time_range(name)` | `Option<(u64, u64)>` | Buffer window (oldest_ns, newest_ns) | --- ## Diagnostics ### TransformFrameStats ```rust let stats = tf.stats(); println!("{}", stats); // Pretty-printed summary println!("Total: {}, Static: {}, Dynamic: {}", stats.total_frames, stats.static_frames, stats.dynamic_frames); println!("Tree depth: {}, Roots: {}", stats.tree_depth, stats.root_count); ``` | Field | Type | Description | |-------|------|-------------| | `total_frames` | `usize` | Total registered frames | | `static_frames` | `usize` | Static frame count | | `dynamic_frames` | `usize` | Dynamic frame count | | `max_frames` | `usize` | Configured max | | `history_len` | `usize` | History buffer size | | `tree_depth` | `usize` | Maximum tree depth | | `root_count` | `usize` | Frames with no parent | ### FrameInfo ```rust if let Some(info) = tf.frame_info("camera") { println!("Frame: {}, Parent: {:?}, Static: {}", info.name, info.parent, info.is_static); println!("Depth: {}, Children: {}", info.depth, info.children_count); } // All frames let all = tf.frame_info_all(); ``` | Field | Type | Description | |-------|------|-------------| | `name` | `String` | Frame name | | `id` | `FrameId` | Numeric ID (for hot-path lookups) | | `parent` | `Option` | Parent name (`None` = root) | | `is_static` | `bool` | Static vs dynamic | | `time_range` | `Option<(u64, u64)>` | (oldest_ns, newest_ns) | | `children_count` | `usize` | Direct children | | `depth` | `usize` | Depth in tree (root = 0) | ### Tree Export ```rust tf.print_tree(); // Print to stderr let text = tf.format_tree(); // Human-readable tree string let dot = tf.frames_as_dot(); // Graphviz DOT format let yaml = tf.frames_as_yaml(); // YAML (TF2-style) tf.validate()?; // Validate tree structure ``` --- ## timestamp_now() ```rust use horus::prelude::*; let now: u64 = timestamp_now(); // Current time in nanoseconds (UNIX epoch) ``` --- ## FrameId Type alias for `u32`. Use for hot-path lookups to avoid string-based name resolution: ```rust let id: FrameId = tf.frame_id("base_link").unwrap(); // Fast by-ID operations (no string lookup) tf.update_transform_by_id(id, &transform, timestamp_now())?; let t = tf.tf_by_id(src_id, dst_id); ``` --- ## Complete Example ```rust use horus::prelude::*; fn main() -> Result<()> { let tf = TransformFrame::new(); // Build frame tree tf.add_frame("world").build()?; tf.add_frame("odom").parent("world").build()?; tf.add_frame("base_link").parent("odom").build()?; tf.add_frame("lidar") .parent("base_link") .static_transform(&Transform::xyz(0.2, 0.0, 0.3)) .build()?; tf.add_frame("camera") .parent("base_link") .static_transform(&Transform::xyz(0.1, 0.0, 0.5).with_rpy(0.0, 0.1, 0.0)) .build()?; // Update dynamic frames let now = timestamp_now(); tf.update_transform("odom", &Transform::xyz(1.0, 0.5, 0.0).with_yaw(0.3), now)?; tf.update_transform("base_link", &Transform::xyz(0.05, 0.02, 0.0), now)?; // Query transforms let lidar_to_world = tf.query("lidar").to("world").lookup()?; println!("Lidar position in world: {:?}", lidar_to_world.translation); // Transform a LiDAR point into world frame let world_pt = tf.query("lidar").to("world").point([5.0, 0.0, 0.0])?; println!("Obstacle at world: ({:.2}, {:.2}, {:.2})", world_pt[0], world_pt[1], world_pt[2]); // Check staleness if tf.is_stale_now("base_link", 100_000_000) { // 100ms println!("Odometry is stale!"); } // Print tree tf.print_tree(); Ok(()) } ``` --- ## Common Frame Configurations ### Mobile Robot with Camera and Lidar ``` world → odom → base_link → laser_link → camera_link → camera_optical ``` ```rust let tf = TransformFrame::new(); // Static frames (don't change) tf.register_frame("world", None)?; tf.register_frame("odom", Some("world"))?; tf.register_frame("base_link", Some("odom"))?; // Sensor mounts (static transforms from CAD/measurement) tf.register_frame("laser_link", Some("base_link"))?; tf.update_transform("laser_link", &Transform::new([0.15, 0.0, 0.3], [0.0, 0.0, 0.0, 1.0]), timestamp_now())?; tf.register_frame("camera_link", Some("base_link"))?; tf.update_transform("camera_link", &Transform::new([0.1, -0.05, 0.25], [0.0, 0.0, 0.0, 1.0]), timestamp_now())?; // Query: transform lidar point to camera frame let laser_to_cam = tf.tf("laser_link", "camera_link")?; let point_in_cam = laser_to_cam.transform_point([2.0, 0.0, 0.0]); ``` ### Robot Arm (6-DOF) ``` base → link1 → link2 → link3 → link4 → link5 → link6 → tool → gripper ``` ```rust let tf = TransformFrame::new(); tf.register_frame("base", None)?; for i in 1..=6 { let parent = if i == 1 { "base" } else { &format!("link{}", i - 1) }; tf.register_frame(&format!("link{}", i), Some(parent))?; } tf.register_frame("tool", Some("link6"))?; tf.register_frame("gripper", Some("tool"))?; // Update joint angles each tick (from forward kinematics) // tf.update_transform("link1", &fk_transform, timestamp_now())?; // Query: where is the gripper in base frame? let gripper_pose = tf.tf("gripper", "base")?; ``` --- ## See Also - [TransformFrame Concepts](/concepts/transform-frame) — Architecture and design patterns - [Python TransformFrame API](/python/api/transform-frame) — Python bindings - [Scheduler API](/rust/api/scheduler) — Running nodes that use transforms --- ## Large Data Types Path: /rust/api/tensor-pool Description: Working with images, point clouds, and depth data in HORUS # Large Data Types Camera images, LiDAR point clouds, and depth maps are too large to copy through topics. HORUS uses shared memory pools — the data stays in-place, and only a lightweight descriptor (~300 bytes) is sent through the topic. The receiver gets direct access to the same memory. Zero copies, zero serialization. **Quick example** — camera capture to processing pipeline: ```rust use horus::prelude::*; // Camera node: capture and publish (zero-copy send) let mut img = Image::new(1920, 1080, ImageEncoding::Rgb8)?; // ... fill from camera driver ... let topic: Topic = Topic::new("camera.rgb")?; topic.send(&img); // sends ~300B descriptor, not 6MB of pixels // Processing node: receive and access (zero-copy read) let topic: Topic = Topic::new("camera.rgb")?; if let Some(img) = topic.recv() { let pixel = img.pixel(960, 540); // direct memory access, no copy println!("Center pixel: {:?}", pixel); } ``` ## Image `Image` is the primary type for camera data. Pool allocation and memory management are automatic. ```rust use horus::prelude::*; // Create an image let mut img = Image::new(640, 480, ImageEncoding::Rgb8)?; img.fill(&[255, 0, 0]); // Fill with red // Send via topic — only a descriptor is transmitted, not the pixels let topic: Topic = Topic::new("camera.rgb")?; topic.send(&img); ``` ```rust // Receive side let topic: Topic = Topic::new("camera.rgb")?; if let Some(img) = topic.recv() { let pixel = img.pixel(0, 0); // Direct pixel access println!("{}x{}", img.width(), img.height()); } ``` ## PointCloud `PointCloud` handles 3D point data from lidar, depth cameras, or other 3D sensors. ```rust use horus::prelude::*; // Create a point cloud with 10,000 points, 3 channels (x, y, z) let pc = PointCloud::from_xyz(\&points)? // 10000 points; let topic: Topic = Topic::new("lidar.points")?; topic.send(&pc); ``` ## DepthImage `DepthImage` stores per-pixel depth values from depth cameras or stereo systems. ```rust use horus::prelude::*; let depth = DepthImage::meters(480, 640)?; let topic: Topic = Topic::new("camera.depth")?; topic.send(&depth); ``` See [Basic Examples — Camera Image Pipeline](/rust/examples/basic-examples#6-camera-image-pipeline) for complete send/recv examples. ## Supported Data Types (TensorDtype) All large data types accept a `TensorDtype` to specify the element type: | Type | Rust | Size | |------|------|------| | `TensorDtype::F32` | `f32` | 4 bytes | | `TensorDtype::F64` | `f64` | 8 bytes | | `TensorDtype::F16` | `f16` | 2 bytes | | `TensorDtype::BF16` | `bf16` | 2 bytes | | `TensorDtype::I8` | `i8` | 1 byte | | `TensorDtype::I16` | `i16` | 2 bytes | | `TensorDtype::I32` | `i32` | 4 bytes | | `TensorDtype::I64` | `i64` | 8 bytes | | `TensorDtype::U8` | `u8` | 1 byte | | `TensorDtype::U16` | `u16` | 2 bytes | | `TensorDtype::U32` | `u32` | 4 bytes | | `TensorDtype::U64` | `u64` | 8 bytes | | `TensorDtype::Bool` | `bool` | 1 byte | ## Device The `Device` type tags tensors with a target device location. Currently only CPU pools are implemented. ```rust use horus::prelude::*; Device::cpu() // CPU shared memory (default) Device::cuda(0) // CUDA device 0 (descriptor only — GPU pools not yet implemented) ``` ## Performance | Operation | Latency | |-----------|---------| | Slot allocation | ~100ns | | Cross-process access | Zero-copy shared memory | ## Advanced: Raw Tensor Access For cases where `Image`, `PointCloud`, and `DepthImage` don't fit your needs, you can work with tensors directly through `Topic`. The topic auto-manages a shared memory pool per topic name. ```rust use horus::prelude::*; let topic: Topic = Topic::new("custom.data")?; // Allocate from the topic's auto-managed pool let handle = topic.alloc_tensor(&[1080, 1920, 3], TensorDtype::U8, Device::cpu())?; // Write data let data = handle.data_slice_mut()?; // ... fill data ... // Send — only the descriptor is transmitted topic.send_handle(&handle); ``` ```rust // Receiver side let topic: Topic = Topic::new("custom.data")?; if let Some(handle) = topic.recv_handle() { let data = handle.data_slice()?; // Zero-copy access println!("Shape: {:?}, Dtype: {:?}", handle.shape(), handle.dtype()); } // Refcount decremented automatically on drop ``` ## See Also - [Tensor Messages](/rust/api/tensor-messages) - Tensor, TensorDtype, Device types and domain wrappers - [Topic & Shared Memory](/concepts/core-concepts-topic#shared-memory-details) - How HORUS achieves zero-copy IPC - [Python Memory Types](/python/api/memory-types) — Python Image, PointCloud, DepthImage --- ## Geometry Messages Path: /rust/api/geometry-messages Description: 3D and 2D spatial primitives for position, orientation, and motion # Geometry Messages HORUS provides fundamental geometric primitives used throughout robotics applications for representing position, orientation, and motion. All geometry messages support ultra-fast zero-copy transfer (~50ns). ## Twist 3D velocity command with linear and angular components. Used for commanding robot motion in 3D space. For 2D robots, only x (forward) and yaw (rotation) are typically used. ```rust use horus::prelude::*; // Create 3D velocity command let twist = Twist::new( [1.0, 0.0, 0.0], // linear: [x, y, z] in m/s [0.0, 0.0, 0.5] // angular: [roll, pitch, yaw] in rad/s ); // For 2D robots (common case) let cmd = Twist::new_2d(0.5, 0.3); // 0.5 m/s forward, 0.3 rad/s rotation println!("Linear X: {}, Angular Z: {}", cmd.linear[0], cmd.angular[2]); // Stop command (all zeros) let stop = Twist::stop(); // Validate the message if twist.is_valid() { println!("Twist is valid"); } ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | Field | Type | Unit | Description | |-------|------|------|-------------| | `linear` | `[f64; 3]` | m/s | Linear velocity [x, y, z]. X=forward, Y=left, Z=up | | `angular` | `[f64; 3]` | rad/s | Angular velocity [roll, pitch, yaw]. Positive yaw=counter-clockwise | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **Coordinate frame:** Right-hand, Z-up. X=forward, Y=left. Consistent across all geometry types. > > **ROS2 equivalent:** `geometry_msgs/msg/Twist` > > **Typical ranges:** Mobile robots: linear.x ±2.0 m/s, angular.z ±3.0 rad/s **Methods:** | Method | Description | |--------|-------------| | `new(linear, angular)` | Create full 3D velocity command | | `new_2d(linear_x, angular_z)` | Create 2D velocity (forward + rotation) | | `stop()` | Create stop command (all zeros) | | `is_valid()` | Check if all values are finite | ## Pose2D 2D pose representation (position and orientation). Commonly used for mobile robots operating in planar environments. ```rust use horus::prelude::*; // Create 2D pose let pose = Pose2D::new(5.0, 3.0, 1.57); // x, y, theta println!("Position: ({}, {}), Orientation: {} rad", pose.x, pose.y, pose.theta); // Create pose at origin let origin = Pose2D::origin(); // Calculate distance between poses let other = Pose2D::new(8.0, 7.0, 0.0); let distance = pose.distance_to(&other); println!("Distance: {:.2} m", distance); // Normalize angle to [-π, π] let mut pose_copy = pose; pose_copy.normalize_angle(); // Check validity assert!(pose.is_valid()); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x` | `f64` | X position in meters | | `y` | `f64` | Y position in meters | | `theta` | `f64` | Orientation angle in radians | | `timestamp_ns` | `u64` | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new(x, y, theta)` | Create a new 2D pose | | `origin()` | Create pose at origin (0, 0, 0) | | `distance_to(&other)` | Calculate euclidean distance to another pose | | `normalize_angle()` | Normalize theta to [-π, π] | | `is_valid()` | Check if all values are finite | ## TransformStamped 3D transformation (translation and rotation). Represents a full 3D transformation using translation vector and quaternion rotation. Used for coordinate frame transformations. ```rust use horus::prelude::*; // Create transform with translation and quaternion rotation let transform = TransformStamped::new( [1.0, 2.0, 0.5], // translation [x, y, z] [0.0, 0.0, 0.0, 1.0] // rotation [x, y, z, w] quaternion ); // Identity transform (no translation or rotation) let identity = TransformStamped::identity(); // Create from 2D pose (z=0, only yaw rotation) let pose = Pose2D::new(3.0, 4.0, 1.57); let tf_from_pose = TransformStamped::from_pose_2d(&pose); // Validate quaternion is normalized if transform.is_valid() { println!("Transform is valid (quaternion normalized)"); } // Normalize quaternion if needed let mut tf = TransformStamped::new([0.0; 3], [1.0, 1.0, 1.0, 1.0]); tf.normalize_rotation(); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `translation` | `[f64; 3]` | Translation [x, y, z] in meters | | `rotation` | `[f64; 4]` | Rotation as quaternion [x, y, z, w] | | `timestamp_ns` | `u64` | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new(translation, rotation)` | Create new transform | | `identity()` | Identity transform (no change) | | `from_pose_2d(&pose)` | Create from 2D pose | | `is_valid()` | Check quaternion normalized and values finite | | `normalize_rotation()` | Normalize the quaternion component | ## Point3 3D point representation. ```rust use horus::prelude::*; // Create 3D point let point = Point3::new(1.0, 2.0, 3.0); println!("Point: ({}, {}, {})", point.x, point.y, point.z); // Create point at origin let origin = Point3::origin(); // Calculate distance between points let other = Point3::new(4.0, 6.0, 3.0); let distance = point.distance_to(&other); println!("Distance: {:.2} m", distance); // 5.0 m ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x` | `f64` | X coordinate in meters | | `y` | `f64` | Y coordinate in meters | | `z` | `f64` | Z coordinate in meters | **Methods:** | Method | Description | |--------|-------------| | `new(x, y, z)` | Create new point | | `origin()` | Create point at origin (0, 0, 0) | | `distance_to(&other)` | Calculate euclidean distance | ## Vector3 3D vector representation with mathematical operations. ```rust use horus::prelude::*; // Create 3D vector let v = Vector3::new(3.0, 4.0, 0.0); println!("Vector: ({}, {}, {})", v.x, v.y, v.z); // Zero vector let zero = Vector3::zero(); // Calculate magnitude let mag = v.magnitude(); println!("Magnitude: {:.2}", mag); // 5.0 // Normalize vector let mut unit = Vector3::new(3.0, 4.0, 0.0); unit.normalize(); println!("Unit vector: ({:.2}, {:.2}, {})", unit.x, unit.y, unit.z); // (0.6, 0.8, 0) // Dot product let v1 = Vector3::new(1.0, 2.0, 3.0); let v2 = Vector3::new(4.0, 5.0, 6.0); let dot = v1.dot(&v2); println!("Dot product: {}", dot); // 32.0 // Cross product let i = Vector3::new(1.0, 0.0, 0.0); let j = Vector3::new(0.0, 1.0, 0.0); let k = i.cross(&j); println!("i × j = ({}, {}, {})", k.x, k.y, k.z); // (0, 0, 1) ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x` | `f64` | X component | | `y` | `f64` | Y component | | `z` | `f64` | Z component | **Methods:** | Method | Description | |--------|-------------| | `new(x, y, z)` | Create new vector | | `zero()` | Create zero vector | | `magnitude()` | Calculate vector length | | `normalize()` | Normalize to unit vector | | `dot(&other)` | Dot product with another vector | | `cross(&other)` | Cross product with another vector | ## Quaternion Quaternion for 3D rotation representation. Quaternions avoid gimbal lock and provide smooth interpolation for rotations. ```rust use horus::prelude::*; // Create quaternion directly let q = Quaternion::new(0.0, 0.0, 0.0, 1.0); // [x, y, z, w] // Identity quaternion (no rotation) let identity = Quaternion::identity(); assert_eq!(identity.w, 1.0); // Create from Euler angles (roll, pitch, yaw) let q_from_euler = Quaternion::from_euler( 0.0, // roll (rotation around X) 0.0, // pitch (rotation around Y) 1.57 // yaw (rotation around Z) - 90 degrees ); // Normalize quaternion let mut q_unnorm = Quaternion::new(1.0, 1.0, 1.0, 1.0); q_unnorm.normalize(); // Validate assert!(q.is_valid()); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x` | `f64` | X component (imaginary i) | | `y` | `f64` | Y component (imaginary j) | | `z` | `f64` | Z component (imaginary k) | | `w` | `f64` | W component (real/scalar) | **Methods:** | Method | Description | |--------|-------------| | `new(x, y, z, w)` | Create new quaternion | | `identity()` | Identity quaternion (no rotation) | | `from_euler(roll, pitch, yaw)` | Create from Euler angles | | `normalize()` | Normalize to unit quaternion | | `is_valid()` | Check all values are finite | ## Zero-Copy Performance All geometry messages are optimized for zero-copy shared memory transfer (~50ns): ```rust use horus::prelude::*; // HORUS automatically uses zero-copy for compatible types let twist_topic: Topic = Topic::new("velocity_cmd")?; let pose_topic: Topic = Topic::new("robot_pose")?; // Send - automatically zero-copy for fixed-size types twist_topic.send(Twist::new_2d(0.5, 0.1)); // Receive - zero-copy access if let Some(pose) = pose_topic.recv() { println!("Robot at ({:.2}, {:.2})", pose.x, pose.y); } ``` ## Robot Control Example ```rust use horus::prelude::*; struct DifferentialDriveController { pose_sub: Topic, goal_sub: Topic, cmd_pub: Topic, } impl Node for DifferentialDriveController { fn name(&self) -> &str { "DiffDriveController" } fn tick(&mut self) { // Get current pose and goal let pose = match self.pose_sub.recv() { Some(p) => p, None => return, }; let goal = match self.goal_sub.recv() { Some(g) => g, None => return, }; // Calculate distance and angle to goal let dx = goal.x - pose.x; let dy = goal.y - pose.y; let distance = (dx * dx + dy * dy).sqrt(); let angle_to_goal = dy.atan2(dx); let angle_error = angle_to_goal - pose.theta; // Simple proportional controller let cmd = if distance > 0.1 { // Move towards goal Twist::new_2d( 0.3 * distance.min(1.0), // Linear velocity (capped) 1.0 * angle_error // Angular velocity ) } else { // Goal reached Twist::stop() }; self.cmd_pub.send(cmd); } } ``` ## Coordinate Frames HORUS uses right-handed coordinate systems: - **X-axis**: Forward (red) - **Y-axis**: Left (green) - **Z-axis**: Up (blue) For 2D robots: - **X**: Forward - **Y**: Left - **Theta**: Counter-clockwise rotation from X-axis ## See Also - [Navigation Messages](/rust/api/navigation-messages) - Goals, paths, occupancy grids - [Sensor Messages](/rust/api/sensor-messages) - IMU, odometry - [Force Messages](/rust/api/force-messages) - Wrench, force vectors --- ## API Reference Path: /rust/api Description: Complete API reference for the HORUS robotics framework # API Reference Welcome to the HORUS API reference documentation. This section provides detailed documentation for all public types, traits, and functions in the HORUS framework. ## Crates | Crate | Description | |-------|-------------| | [horus_core](/rust/api) | Core runtime - nodes, communication, scheduling | | [horus_library](/rust/api/messages) | Standard message types for robotics | | [horus_macros](/rust/api/macros) | Procedural macros (`node!`, `derive(LogSummary)`) | --- ## Quick Reference ### Core Types | Type | Description | Module | |------|-------------|--------| | [`Node`](/rust/api#node) | Base trait for all computation units | `horus_core` | | [`Topic`](/rust/api/topic) | Unified pub/sub channel with auto-detected backends | `horus_core` | | [`Scheduler`](/rust/api/scheduler) | Node execution orchestrator | `horus_core` | | [`ServiceClient`](/rust/api/services) | Synchronous request/response RPC | `horus_core` | | [`ActionClientNode`](/rust/api/actions) | Long-running tasks with feedback and cancellation | `horus_core` | ### Error Handling | Type | Description | Module | |------|-------------|--------| | [`Error`](/rust/api#error) | Unified error type | `horus_core` | | [`Result`](/rust/api#result) | Result alias | `horus_core` | ### Message Types | Type | Description | Module | |------|-------------|--------| | [`Image`](/rust/api/messages#image) | Camera image data | `horus_library` | | [`LaserScan`](/rust/api/messages#laserscan) | LiDAR scan data | `horus_library` | | [`Imu`](/rust/api/messages#imu) | IMU sensor data | `horus_library` | | [`Twist`](/rust/api/messages#twist) | Velocity commands | `horus_library` | | [`Pose2D`](/rust/api/messages#pose2d) | 2D position and orientation | `horus_library` | --- ## Import Patterns ### Minimal Import ```rust use horus::prelude::*; ``` This single import gives you access to **165+ types** — everything needed for typical robotics applications. ### Explicit Import (Alternative) If you prefer explicit imports: ```rust use horus::prelude::{ // Core traits and types Node, NodeState, HealthStatus, LogSummary, // Communication Topic, // Scheduling Scheduler, FailurePolicy, // Real-time DurationExt, Frequency, Miss, Rate, Stopwatch, // Errors Error, Result, HorusError, // Messages Pose2D, LaserScan, Imu, CmdVel, MotorCommand, }; ``` --- ## Prelude Contents The `horus::prelude` re-exports everything you need without hunting for module paths. Here is the full inventory, grouped by category. ### Core Traits & Types | Type | Kind | Description | |------|------|-------------| | `Node` | trait | Base trait for all computation units | | `NodeState` | enum | Running, Paused, Stopped, Error | | `HealthStatus` | enum | Node health reporting | | `LogSummary` | trait | Structured logging for nodes | ### Communication | Type | Kind | Description | |------|------|-------------| | `Topic` | struct | Typed pub/sub channel with auto-detected backends | ### Scheduling & Execution | Type | Kind | Description | |------|------|-------------| | `Scheduler` | struct | Node execution orchestrator | | `FailurePolicy` | enum | Fatal, Restart, Skip, Ignore | ### Real-Time & Timing | Type | Kind | Description | |------|------|-------------| | `DurationExt` | trait | Ergonomic duration creation: `200.us()`, `1.ms()` | | `Frequency` | struct | Type-safe frequency: `100.hz()` | | `Miss` | enum | Deadline miss policy: Warn, Skip, SafeMode, Stop | | `RtStats` | struct | Real-time execution statistics | | `Rate` | struct | Fixed-rate loop control | | `Stopwatch` | struct | High-resolution timer | | [`RuntimeParams`](/rust/api/runtime-params) | struct | Runtime parameter store | ### Memory Domain Types | Type | Kind | Description | |------|------|-------------| | `Image` | struct | Pool-backed camera image (zero-copy) | | `DepthImage` | struct | Pool-backed depth image (F32/U16) | | `PointCloud` | struct | Pool-backed 3D point cloud (zero-copy) | ### Transform Frame | Type | Kind | Description | |------|------|-------------| | `TransformFrame` | struct | Coordinate frame tree manager | | `TransformFrameConfig` | struct | Frame tree configuration | | `TransformFrameStats` | struct | Frame tree statistics | | `Transform` | struct | 3D rigid transformation | | `TransformQuery` | struct | Transform lookup query | | `TransformQueryFrom` | struct | Transform query builder | | `FrameBuilder` | struct | Fluent frame registration | | `FrameInfo` | struct | Frame metadata | | `timestamp_now()` | fn | Current time in nanoseconds | ### Geometry Messages `Accel`, `AccelStamped`, `Point3`, `Pose2D`, `Pose3D`, `PoseStamped`, `PoseWithCovariance`, `Quaternion`, `TransformStamped`, `Twist`, `TwistWithCovariance`, `Vector3` ### Sensor Messages `BatteryState`, `FluidPressure`, `Illuminance`, `Imu`, `JointState`, `LaserScan`, `MagneticField`, `NavSatFix`, `Odometry`, `RangeSensor`, `Temperature` ### Clock & Time Messages `Clock`, `TimeReference`, `SOURCE_WALL`, `SOURCE_SIM`, `SOURCE_REPLAY` ### Control & Actuator Messages `CmdVel`, `DifferentialDriveCommand`, `JointCommand`, `MotorCommand`, `PidConfig`, `ServoCommand`, `TrajectoryPoint` ### Diagnostics Messages `DiagnosticReport`, `DiagnosticStatus`, `DiagnosticValue`, `EmergencyStop`, `Heartbeat`, `NodeHeartbeat`, `NodeStateMsg`, `ResourceUsage`, `SafetyStatus`, `StatusLevel` ### Vision & Perception Messages `BoundingBox2D`, `BoundingBox3D`, `CameraInfo`, `CompressedImage`, `Detection`, `Detection3D`, `Landmark`, `Landmark3D`, `LandmarkArray`, `PlaneDetection`, `RegionOfInterest`, `SegmentationMask`, `TrackedObject`, `TrackingHeader` ### Navigation Messages `CostMap`, `NavGoal`, `NavPath`, `OccupancyGrid`, `PathPlan` ### Force & Impedance Messages `ForceCommand`, `ImpedanceParameters`, `WrenchStamped` ### Input Messages `JoystickInput`, `KeyboardInput` ### Application Messages `CmdVel`, `GenericMessage` (flexible cross-language messaging, `MAX_GENERIC_PAYLOAD = 4096`) ### Type Helpers `Device`, `ImageEncoding`, `PointXYZ`, `PointXYZI`, `PointXYZRGB`, `TensorDtype` ### Actions `Action`, `ActionClient`, `ActionClientBuilder`, `ActionClientNode`, `ActionError`, `ActionResult`, `ActionServerBuilder`, `ActionServerNode`, `CancelResponse`, `ClientGoalHandle`, `GoalId`, `GoalOutcome`, `GoalPriority`, `GoalResponse`, `GoalStatus`, `PreemptionPolicy`, `ServerGoalHandle` ### Services `AsyncServiceClient`, `Service`, `ServiceClient`, `ServiceError`, `ServiceRequest`, `ServiceResponse`, `ServiceResult`, `ServiceServer`, `ServiceServerBuilder` ### Error Handling `Error`, `Result`, `HorusError`, `HorusContext`, `CommunicationError`, `ConfigError`, `MemoryError`, `NodeError`, `NotFoundError`, `ParseError`, `ResourceError`, `SerializationError`, `Severity`, `TimeoutError`, `TransformError`, `ValidationError`, `retry_transient()`, `RetryConfig` ### Macros (with `"macros"` feature) | Macro | Description | |-------|-------------| | `message!` | Define custom message types | | `service!` | Define request/response service types | | `action!` | Define long-running action types (goal/feedback/result) | | `standard_action!` | Pre-built action templates | | `hlog!` | Structured node logging | | `hlog_once!` | Log once per program execution | | `hlog_every!` | Throttled logging | | `node!` | Define node with automatic topic registration | --- ## Version Compatibility | HORUS Version | Rust Edition | MSRV | |---------------|--------------|------| | 0.1.x | 2021 | 1.92.0 | --- ## See Also - [Cargo Feature Flags](/rust/api/feature-flags) - Feature flags across all HORUS crates - [Core Concepts](/concepts/architecture) - Understanding HORUS architecture - [Examples](/rust/examples/basic-examples) - Working code examples - [Message Types](/concepts/message-types) - Standard message reference --- ## Perception Messages Path: /rust/api/perception-messages Description: 3D perception, point cloud, depth, landmark, tracking, and segmentation message types # Perception Messages HORUS provides message types for 3D perception tasks. These include: - **Pool-backed RAII types**: `PointCloud`, `DepthImage` — zero-copy shared memory backed, managed via global tensor pool - **Serde types**: `PointField`, `PlaneDetection` — flexible, support serialization - **Zero-copy types**: `PointXYZ`, `Landmark`, `TrackedObject`, `SegmentationMask` — fixed-size, suitable for shared memory transport with zero serialization overhead ## PointCloud Pool-backed RAII point cloud type with zero-copy shared memory transport. PointCloud allocates from a global tensor pool. ```rust use horus::prelude::*; // Create XYZ point cloud (10000 points, 3 floats per point) let mut cloud = PointCloud::from_xyz(\&points)? // 10000 points; // Copy point data let point_data: Vec = vec![0; 10000 * 3 * 4]; // 10000 points * 3 floats * 4 bytes cloud.copy_from(&point_data); // Set metadata (method chaining) cloud.set_frame_id("lidar_front").set_timestamp_ns(1234567890); // Access properties println!("Points: {}", cloud.point_count()); println!("Fields per point: {}", cloud.fields_per_point()); println!("Is XYZ: {}", cloud.is_xyz()); // Zero-copy data access let data: &[u8] = cloud.data(); // Extract XYZ coordinates as Vec<[f32; 3]> if let Some(points) = cloud.extract_xyz() { for p in &points[..3] { println!("({:.2}, {:.2}, {:.2})", p[0], p[1], p[2]); } } // Access individual point if let Some(point_bytes) = cloud.point_at(0) { println!("Point 0: {} bytes", point_bytes.len()); } ``` **PointCloud methods:** PointCloud is an RAII type — fields are private, accessed through methods. Mutation methods return `&mut Self` for chaining. | Method | Returns | Description | |--------|---------|-------------| | `new(num_points, fields_per_point, dtype)` | `Result` | Create point cloud (allocates from global pool) | | `data()` | `&[u8]` | Zero-copy access to raw point data | | `data_mut()` | `&mut [u8]` | Mutable access to point data | | `copy_from(src)` | `&mut Self` | Copy data into point cloud | | `point_at(idx)` | `Option<&[u8]>` | Get bytes for the i-th point | | `extract_xyz()` | `Option>` | Extract XYZ as float arrays (F32 only) | | `point_count()` | `u64` | Number of points | | `fields_per_point()` | `u32` | Floats per point (3=XYZ, 4=XYZI, 6=XYZRGB) | | `dtype()` | `TensorDtype` | Data type of point components | | `is_xyz()` | `bool` | Whether this is a plain XYZ cloud | | `has_intensity()` | `bool` | Whether cloud has intensity field | | `set_frame_id(id)` | `&mut Self` | Set sensor frame identifier | | `set_timestamp_ns(ts)` | `&mut Self` | Set timestamp in nanoseconds | ### PointField Describes a field within point cloud data (serde type, used for custom point formats): ```rust use horus::prelude::*; // Create custom field let intensity = PointField::new("intensity", 12, TensorDtype::F32, 1); println!("Field: {}, size: {} bytes", intensity.name_str(), intensity.field_size()); ``` **PointField** uses `TensorDtype` (available via prelude) for its `datatype` field. See [TensorDtype values](/rust/api/tensor#tensordtype) for the full list. ## Point Types (Zero-Copy) Fixed-size point types suitable for zero-copy shared memory transport. ### PointXYZ Basic 3D point (12 bytes). ```rust use horus::prelude::*; let point = PointXYZ::new(1.0, 2.0, 3.0); println!("Distance from origin: {:.2}m", point.distance()); let other = PointXYZ::new(4.0, 6.0, 3.0); println!("Distance between: {:.2}m", point.distance_to(&other)); ``` | Field | Type | Description | |-------|------|-------------| | `x` | `f32` | X coordinate (meters) | | `y` | `f32` | Y coordinate (meters) | | `z` | `f32` | Z coordinate (meters) | ### PointXYZRGB 3D point with RGB color (16 bytes). Common for RGB-D cameras like Intel RealSense. ```rust use horus::prelude::*; let point = PointXYZRGB::new(1.0, 2.0, 3.0, 255, 0, 0); // Red point // Convert from PointXYZ (defaults to white) let xyz = PointXYZ::new(1.0, 2.0, 3.0); let colored = PointXYZRGB::from_xyz(xyz); // Get packed RGB as u32 (0xRRGGBBAA) let packed = point.rgb_packed(); // Convert back to PointXYZ (drop color) let xyz_only = point.xyz(); ``` | Field | Type | Description | |-------|------|-------------| | `x`, `y`, `z` | `f32` | Coordinates (meters) | | `r`, `g`, `b` | `u8` | Color components (0-255) | | `a` | `u8` | Alpha/padding (255 default) | ### PointXYZI 3D point with intensity (16 bytes). Common for LiDAR sensors (Velodyne, Ouster, Livox). ```rust use horus::prelude::*; let point = PointXYZI::new(1.0, 2.0, 3.0, 128.0); // Convert from PointXYZ (zero intensity) let xyz = PointXYZ::new(1.0, 2.0, 3.0); let with_intensity = PointXYZI::from_xyz(xyz); ``` | Field | Type | Description | |-------|------|-------------| | `x`, `y`, `z` | `f32` | Coordinates (meters) | | `intensity` | `f32` | Reflectance (typically 0-255) | ### PointCloudHeader Header for array transmission of point clouds (64 bytes). Sent before the point data array via IPC. ```rust use horus::prelude::*; // Create header for different point types let header = PointCloudHeader::xyz(10000) .with_frame_id("lidar_front") .with_timestamp(timestamp_ns); // Or for colored points let header = PointCloudHeader::xyzrgb(5000); // Calculate total data size println!("Data size: {} bytes", header.data_size()); ``` | Field | Type | Description | |-------|------|-------------| | `num_points` | `u64` | Number of points | | `point_type` | `u32` | 0=XYZ, 1=XYZRGB, 2=XYZI | | `point_stride` | `u32` | Bytes per point | | `timestamp_ns` | `u64` | Nanoseconds since epoch | | `seq` | `u64` | Sequence number | | `frame_id` | `[u8; 32]` | Sensor/coordinate frame | ## DepthImage Pool-backed RAII depth image with zero-copy shared memory transport. Supports both F32 (meters) and U16 (millimeters) formats. ```rust use horus::prelude::*; // Create F32 depth image (meters) let mut depth = DepthImage::meters(640, 480)?; // Or U16 depth image (millimeters) let mut depth_u16 = DepthImage::millimeters(640, 480)?; // Set metadata (method chaining) depth.set_frame_id("depth_camera").set_timestamp_ns(1234567890); // Access depth at pixel (always returns meters as f32) if let Some(d) = depth.get_depth(320, 240) { println!("Depth at center: {:.3}m", d); } // Set depth at pixel (value in meters) depth.set_depth(100, 100, 1.5); // Get raw U16 value for millimeter-format images if let Some(mm) = depth_u16.get_depth_u16(320, 240) { println!("Raw depth: {}mm", mm); } // Get statistics (min, max, mean in meters) if let Some((min, max, mean)) = depth.depth_statistics() { println!("Depth range: {:.2}-{:.2}m, mean: {:.2}m", min, max, mean); } // Zero-copy data access let data: &[u8] = depth.data(); println!("Image: {}x{}, {} bytes", depth.width(), depth.height(), data.len()); ``` **DepthImage methods:** DepthImage is an RAII type — fields are private, accessed through methods. Mutation methods return `&mut Self` for chaining. | Method | Returns | Description | |--------|---------|-------------| | `new(width, height, dtype)` | `Result` | Create depth image (F32 or U16) | | `data()` | `&[u8]` | Zero-copy access to raw depth data | | `data_mut()` | `&mut [u8]` | Mutable access to depth data | | `get_depth(x, y)` | `Option` | Get depth in meters at pixel | | `set_depth(x, y, value)` | `&mut Self` | Set depth in meters at pixel | | `get_depth_u16(x, y)` | `Option` | Get raw U16 value (millimeter format only) | | `depth_statistics()` | `Option<(f32, f32, f32)>` | Get (min, max, mean) of valid depths in meters | | `width()` | `u32` | Image width in pixels | | `height()` | `u32` | Image height in pixels | | `set_frame_id(id)` | `&mut Self` | Set camera frame identifier | | `set_timestamp_ns(ts)` | `&mut Self` | Set timestamp in nanoseconds | ## PlaneDetection Detected planar surface. ```rust use horus::prelude::*; // Create plane detection (floor plane) let coefficients = [0.0, 0.0, 1.0, 0.0]; // ax + by + cz + d = 0 let center = Point3::new(0.0, 0.0, 0.0); let normal = Vector3::new(0.0, 0.0, 1.0); let plane = PlaneDetection::new(coefficients, center, normal) .with_type("floor"); // Check distance from point to plane let test_point = Point3::new(1.0, 2.0, 0.1); let distance = plane.distance_to_point(&test_point); // Check if point is on plane (within tolerance) if plane.contains_point(&test_point, 0.05) { println!("Point is on the plane"); } println!("Plane type: {}", plane.plane_type_str()); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `coefficients` | `[f64; 4]` | Plane equation [a, b, c, d] | | `center` | `Point3` | Plane center point | | `normal` | `Vector3` | Plane normal vector | | `size` | `[f64; 2]` | Plane bounds (width, height) | | `inlier_count` | `u32` | Number of inlier points | | `confidence` | `f32` | Detection confidence (0-1) | | `plane_type` | `[u8; 16]` | Type label ("floor", "wall", etc.) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ### PlaneArray Array of detected planes (max 16). | Field | Type | Description | |-------|------|-------------| | `planes` | `[PlaneDetection; 16]` | Array of plane detections | | `count` | `u8` | Number of valid planes | | `frame_id` | `[u8; 32]` | Source sensor frame | | `algorithm` | `[u8; 32]` | Detection algorithm used | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## Landmark (Zero-Copy) 2D landmark/keypoint for pose estimation, facial landmarks, hand tracking. Fixed-size (16 bytes). ```rust use horus::prelude::*; // Create landmark with visibility let nose = Landmark::new(320.0, 240.0, 0.95, 0); // (x, y, visibility, index) // Create visible landmark (visibility = 1.0) let eye = Landmark::visible(300.0, 220.0, 1); // left_eye // Check visibility if nose.is_visible(0.5) { println!("Nose detected at ({}, {})", nose.x, nose.y); } // Distance between landmarks let distance = nose.distance_to(&eye); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x` | `f32` | X coordinate (pixels or normalized 0-1) | | `y` | `f32` | Y coordinate (pixels or normalized 0-1) | | `visibility` | `f32` | Visibility/confidence (0.0-1.0) | | `index` | `u32` | Landmark index (joint ID) | ### Landmark3D 3D landmark for 3D pose estimation, MediaPipe-style landmarks. Fixed-size (20 bytes). ```rust use horus::prelude::*; let landmark = Landmark3D::new(0.5, 0.3, 0.8, 0.95, 0); // Project to 2D (drops Z) let landmark_2d = landmark.to_2d(); // 3D distance let other = Landmark3D::visible(0.6, 0.4, 0.9, 1); let dist = landmark.distance_to(&other); ``` | Field | Type | Description | |-------|------|-------------| | `x`, `y`, `z` | `f32` | Coordinates (meters or normalized) | | `visibility` | `f32` | Visibility/confidence (0.0-1.0) | | `index` | `u32` | Landmark index | ### LandmarkArray Header for landmark array transmission. Fixed-size (40 bytes). Includes presets for common formats. ```rust use horus::prelude::*; // Standard pose estimation formats let coco = LandmarkArray::coco_pose(); // 17 2D landmarks let mp_pose = LandmarkArray::mediapipe_pose(); // 33 3D landmarks let mp_hand = LandmarkArray::mediapipe_hand(); // 21 3D landmarks let mp_face = LandmarkArray::mediapipe_face(); // 468 3D landmarks // Custom array with metadata let header = LandmarkArray::new_2d(17) .with_confidence(0.92) .with_bbox(100.0, 50.0, 200.0, 300.0); println!("Data size: {} bytes", header.data_size()); ``` **COCO Pose landmark indices** (available as constants in `landmark::coco`): | Index | Landmark | Index | Landmark | |-------|----------|-------|----------| | 0 | Nose | 9 | Left wrist | | 1 | Left eye | 10 | Right wrist | | 2 | Right eye | 11 | Left hip | | 3 | Left ear | 12 | Right hip | | 4 | Right ear | 13 | Left knee | | 5 | Left shoulder | 14 | Right knee | | 6 | Right shoulder | 15 | Left ankle | | 7 | Left elbow | 16 | Right ankle | | 8 | Right elbow | | | ## TrackedObject (Zero-Copy) Multi-object tracking result with Kalman filter prediction. Fixed-size (96 bytes). ```rust use horus::prelude::*; // Create tracked object let bbox = TrackingBBox::new(100.0, 100.0, 50.0, 50.0); let mut track = TrackedObject::new(1, bbox, 0, 0.95); track.set_class_name("person"); // Track lifecycle assert!(track.is_tentative()); // New tracks start tentative track.confirm(); // Promote to confirmed assert!(track.is_confirmed()); // Update with new detection track.update(TrackingBBox::new(110.0, 105.0, 50.0, 50.0), 0.93); println!("Velocity: ({}, {})", track.velocity_x, track.velocity_y); println!("Speed: {:.1}", track.speed()); println!("Heading: {:.2} rad", track.heading()); // Handle missed detection track.mark_missed(); println!("Predicted position: ({}, {})", track.predicted_bbox.x, track.predicted_bbox.y); ``` **TrackedObject fields:** | Field | Type | Description | |-------|------|-------------| | `bbox` | `TrackingBBox` | Current bounding box | | `predicted_bbox` | `TrackingBBox` | Predicted next position (Kalman) | | `track_id` | `u64` | Persistent tracking ID | | `confidence` | `f32` | Detection confidence (0.0-1.0) | | `class_id` | `u32` | Class ID | | `velocity_x`, `velocity_y` | `f32` | Velocity (pixels/frame or m/s) | | `accel_x`, `accel_y` | `f32` | Acceleration | | `age` | `u32` | Frames since first detection | | `hits` | `u32` | Frames with detection | | `time_since_update` | `u32` | Consecutive frames without detection | | `state` | `u32` | 0=tentative, 1=confirmed, 2=deleted | | `class_name` | `[u8; 16]` | Class name (max 15 chars) | **TrackingBBox fields (16 bytes):** | Field | Type | Description | |-------|------|-------------| | `x`, `y` | `f32` | Top-left corner (pixels) | | `width`, `height` | `f32` | Dimensions (pixels) | **TrackingHeader fields (32 bytes):** | Field | Type | Description | |-------|------|-------------| | `num_tracks` | `u32` | Number of tracked objects | | `frame_id` | `u32` | Frame number | | `timestamp_ns` | `u64` | Nanoseconds since epoch | | `total_tracks` | `u64` | Total tracks created | | `active_tracks` | `u32` | Active confirmed tracks | ## SegmentationMask (Zero-Copy) Header for segmentation masks. Fixed-size (64 bytes). The mask pixel data follows the header as a raw byte array. ```rust use horus::prelude::*; // Semantic segmentation (class ID per pixel) let mask = SegmentationMask::semantic(1920, 1080, 80) .with_frame_id("camera_front") .with_timestamp(timestamp_ns); println!("Data size: {} bytes", mask.data_size()); // 1920*1080 // Instance segmentation let mask = SegmentationMask::instance(640, 480); // Panoptic segmentation let mask = SegmentationMask::panoptic(640, 480, 80); // Check type if mask.is_panoptic() { println!("Panoptic mask, u16 data: {} bytes", mask.data_size_u16()); } ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `width` | `u32` | Image width | | `height` | `u32` | Image height | | `num_classes` | `u32` | Number of classes (semantic/panoptic) | | `mask_type` | `u32` | 0=semantic, 1=instance, 2=panoptic | | `timestamp_ns` | `u64` | Nanoseconds since epoch | | `seq` | `u64` | Sequence number | | `frame_id` | `[u8; 32]` | Camera/coordinate frame | **Common COCO class IDs** (available as constants in `segmentation::classes`): `BACKGROUND(0)`, `PERSON(1)`, `BICYCLE(2)`, `CAR(3)`, `MOTORCYCLE(4)`, `BUS(6)`, `TRAIN(7)`, `TRUCK(8)` ## Perception Pipeline Example ```rust use horus::prelude::*; struct PerceptionNode { depth_sub: Topic, cloud_pub: Topic, fx: f32, fy: f32, cx: f32, cy: f32, } impl Node for PerceptionNode { fn name(&self) -> &str { "PerceptionNode" } fn tick(&mut self) { if let Some(depth) = self.depth_sub.recv() { // Read depth values and build point cloud let w = depth.width(); let h = depth.height(); let num_points = (w * h) as u32; if let Ok(mut cloud) = PointCloud::new(num_points, 3, TensorDtype::F32) { // Depth-to-pointcloud conversion would fill cloud data here // using camera intrinsics (fx, fy, cx, cy) self.cloud_pub.send(cloud); } } } } ``` ## PointField Describes a single field within a point cloud's point structure. Used to define custom point formats beyond XYZ. **Fields:** | Field | Type | Description | |-------|------|-------------| | `name` | `[u8; 16]` | Field name (e.g., "x", "y", "z", "intensity", "rgb") | | `offset` | `u32` | Byte offset within the point data structure | | `datatype` | `TensorDtype` | Data type (F32, U8, etc.) | | `count` | `u32` | Number of elements (1 for scalar, 3 for vector) | ```rust use horus::prelude::*; let fields = vec![ PointField::new("x", 0, TensorDtype::F32, 1), PointField::new("y", 4, TensorDtype::F32, 1), PointField::new("z", 8, TensorDtype::F32, 1), PointField::new("intensity", 12, TensorDtype::F32, 1), ]; ``` ## PlaneArray Collection of detected planes (up to 16). Typically output from plane segmentation algorithms (RANSAC, region growing). **Fields:** | Field | Type | Description | |-------|------|-------------| | `planes` | `[PlaneDetection; 16]` | Array of detected planes | | `count` | `u8` | Number of valid planes (0-16) | | `frame_id` | `[u8; 32]` | Source sensor coordinate frame | | `algorithm` | `[u8; 32]` | Detection algorithm name | | `timestamp_ns` | `u64` | Timestamp in nanoseconds since epoch | ## TrackingHeader Metadata header for multi-object tracking. Accompanies tracked object lists with frame-level statistics. **Fields:** | Field | Type | Description | |-------|------|-------------| | `num_tracks` | `u32` | Number of tracked objects in this frame | | `frame_id` | `u32` | Sequential frame number | | `timestamp_ns` | `u64` | Timestamp in nanoseconds since epoch | | `total_tracks` | `u64` | Total tracks ever created (for unique ID generation) | | `active_tracks` | `u32` | Currently confirmed active tracks | ```rust use horus::prelude::*; let header = TrackingHeader::new(5, 42); // 5 tracks, frame 42 println!("Active: {}, Total created: {}", header.active_tracks, header.total_tracks); ``` ## See Also - [Vision Messages](/rust/api/vision-messages) - Image, CameraInfo, Detection, Detection3D - [Message Types](/concepts/message-types) - Standard message type overview - [Sensor Messages](/rust/api/sensor-messages) - Sensor data types --- ## Force & Haptic Messages Path: /rust/api/force-messages Description: Force sensing, impedance control, and haptic feedback # Force & Haptic Messages HORUS provides message types for force/torque sensors, impedance control, and haptic feedback systems commonly used in manipulation tasks. **Re-exported types** (available via `use horus::prelude::*`): `WrenchStamped`, `ImpedanceParameters`, `ForceCommand`. **Non-re-exported types** (require direct import): `ContactInfo`, `ContactState`, `HapticFeedback` — import from `horus_library::messages::force::*`. ## WrenchStamped 6-DOF force and torque measurement from force/torque sensors. ```rust use horus::prelude::*; // Create wrench measurement let force = Vector3::new(10.0, 5.0, -2.0); // Newtons let torque = Vector3::new(0.1, 0.2, 0.05); // Newton-meters let wrench = WrenchStamped::new(force, torque) .with_frame_id("tool0"); // Check magnitudes println!("Force magnitude: {:.2} N", wrench.force_magnitude()); println!("Torque magnitude: {:.3} Nm", wrench.torque_magnitude()); // Safety check let max_force = 50.0; // N let max_torque = 5.0; // Nm if wrench.exceeds_limits(max_force, max_torque) { println!("Safety limits exceeded!"); } // Create from force only let force_only = WrenchStamped::force_only(Vector3::new(0.0, 0.0, -10.0)); // Create from torque only let torque_only = WrenchStamped::torque_only(Vector3::new(0.0, 0.0, 0.5)); // Low-pass filter noisy sensor readings let mut current = wrench; current.filter(&previous_wrench, 0.1); // alpha = 0.1 (heavy filtering) ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `force` | `Vector3` | N | Force [fx, fy, fz] | | `torque` | `Vector3` | Nm | Torque [tx, ty, tz] | | `point_of_application` | `Point3` | m | Force application point | | `frame_id` | `[u8; 32]` | — | Reference frame | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `geometry_msgs/msg/WrenchStamped` **Methods:** | Method | Description | |--------|-------------| | `new(force, torque)` | Create a new wrench measurement | | `force_only(force)` | Create from force only (zero torque) | | `torque_only(torque)` | Create from torque only (zero force) | | `with_frame_id(frame_id)` | Set reference frame (builder pattern) | | `force_magnitude()` | Get force vector magnitude | | `torque_magnitude()` | Get torque vector magnitude | | `exceeds_limits(max_force, max_torque)` | Check if limits exceeded | | `filter(&prev_wrench, alpha)` | Apply low-pass filter (alpha 0.0-1.0) | ## ImpedanceParameters Impedance control parameters for compliant manipulation. ```rust use horus::prelude::*; // Default impedance (moderate compliance) let mut impedance = ImpedanceParameters::new(); // Compliant mode (low stiffness - for delicate tasks) let compliant = ImpedanceParameters::compliant(); // stiffness: [100, 100, 100, 10, 10, 10] // damping: [20, 20, 20, 2, 2, 2] // Stiff mode (high stiffness - for precision tasks) let stiff = ImpedanceParameters::stiff(); // stiffness: [5000, 5000, 5000, 500, 500, 500] // damping: [100, 100, 100, 10, 10, 10] // Enable/disable impedance.enable(); impedance.disable(); // Custom parameters impedance.stiffness = [500.0, 500.0, 200.0, 50.0, 50.0, 50.0]; // [Kx, Ky, Kz, Krx, Kry, Krz] impedance.damping = [30.0, 30.0, 20.0, 3.0, 3.0, 3.0]; // [Dx, Dy, Dz, Drx, Dry, Drz] impedance.force_limits = [50.0, 50.0, 30.0, 5.0, 5.0, 5.0]; // Safety limits ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `stiffness` | `[f64; 6]` | N/m, Nm/rad | Stiffness [Kx, Ky, Kz, Krx, Kry, Krz] | | `damping` | `[f64; 6]` | Ns/m, Nms/rad | Damping [Dx, Dy, Dz, Drx, Dry, Drz] | | `inertia` | `[f64; 6]` | kg, kgm² | Virtual inertia | | `force_limits` | `[f64; 6]` | N, Nm | Force/torque limits | | `enabled` | `u8` | — | Impedance control active (0 = off, 1 = on) | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new()` | Create with default moderate stiffness/damping | | `compliant()` | Create with low stiffness for delicate tasks | | `stiff()` | Create with high stiffness for precision tasks | | `enable()` | Enable impedance control | | `disable()` | Disable impedance control | ## ForceCommand Hybrid force/position control command. ```rust use horus::prelude::*; // Pure force command let force_cmd = ForceCommand::force_only(Vector3::new(0.0, 0.0, -5.0)); // 5N downward // Hybrid force/position control // Force control on Z axis, position control on X/Y let force_axes: [u8; 6] = [0, 0, 1, 0, 0, 0]; // 1=force, 0=position per axis let target_force = Vector3::new(0.0, 0.0, -10.0); let target_position = Vector3::new(0.5, 0.3, 0.0); let hybrid_cmd = ForceCommand::hybrid(force_axes, target_force, target_position); // Surface contact following let surface_normal = Vector3::new(0.0, 0.0, 1.0); // Horizontal surface let contact_force = 5.0; // 5N contact force let surface_cmd = ForceCommand::surface_contact(contact_force, surface_normal); // Set timeout let cmd_with_timeout = force_cmd.with_timeout(5.0); // 5 second timeout ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `target_force` | `Vector3` | N | Desired force | | `target_torque` | `Vector3` | Nm | Desired torque | | `force_mode` | `[u8; 6]` | — | 1 = force control, 0 = position control per axis | | `position_setpoint` | `Vector3` | m | Position target for position-controlled axes | | `orientation_setpoint` | `Vector3` | rad | Orientation target (Euler angles) | | `max_deviation` | `Vector3` | m | Maximum position deviation | | `gains` | `[f64; 6]` | — | Control gains | | `timeout_seconds` | `f64` | s | Command timeout (0 = no timeout) | | `frame_id` | `[u8; 32]` | — | Reference frame | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `geometry_msgs/msg/Wrench` (force/torque portion) **Methods:** | Method | Description | |--------|-------------| | `force_only(target_force)` | Create pure force command (all axes force-controlled) | | `hybrid(force_axes, target_force, target_position)` | Create hybrid force/position command | | `surface_contact(normal_force, surface_normal)` | Create surface following command | | `with_timeout(seconds)` | Set command timeout (builder pattern) | ## ContactInfo Contact detection and classification. > **Note:** `ContactInfo` and `ContactState` are not re-exported in the prelude. Import directly: `use horus_library::messages::force::{ContactInfo, ContactState};` ```rust use horus_library::messages::force::{ContactInfo, ContactState}; // Create contact info let contact = ContactInfo::new(ContactState::StableContact, 15.0); // 15N force // Check contact state if contact.is_in_contact() { println!("Contact force: {:.1} N", contact.contact_force); println!("Duration: {:.2}s", contact.contact_duration_seconds()); println!("Confidence: {:.0}%", contact.confidence * 100.0); } ``` **ContactState values:** | State | Value | Description | |-------|-------|-------------| | `NoContact` | 0 | No contact detected (default) | | `InitialContact` | 1 | First contact moment | | `StableContact` | 2 | Established contact | | `ContactLoss` | 3 | Contact being broken | | `Sliding` | 4 | Sliding contact | | `Impact` | 5 | Impact detected | **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `state` | `u8` | — | Contact state (use `ContactState as u8` to set) | | `contact_force` | `f64` | N | Contact force magnitude | | `contact_normal` | `Vector3` | — | Contact normal vector (estimated, unit vector) | | `contact_point` | `Point3` | m | Contact point (estimated) | | `stiffness` | `f64` | N/m | Contact stiffness (estimated) | | `damping` | `f64` | Ns/m | Contact damping (estimated) | | `confidence` | `f32` | — | Detection confidence (0.0 to 1.0) | | `contact_start_time` | `u64` | ns | Time contact was first detected | | `frame_id` | `[u8; 32]` | — | Reference frame | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new(state, force_magnitude)` | Create new contact info (takes `ContactState`, stores as `u8`) | | `is_in_contact()` | Check if currently in contact (InitialContact, StableContact, or Sliding) | | `contact_duration_seconds()` | Get contact duration in seconds | ## HapticFeedback Haptic feedback commands for user interfaces. > **Note:** `HapticFeedback` is not re-exported in the prelude. Import directly: `use horus_library::messages::force::HapticFeedback;` ```rust use horus::prelude::*; use horus_library::messages::force::HapticFeedback; // Vibration feedback let vibration = HapticFeedback::vibration( 0.8, // intensity (0-1) 250.0, // frequency (Hz) 0.5 // duration (seconds) ); // Force feedback let force = HapticFeedback::force( Vector3::new(1.0, 0.0, 0.0), // Force direction 1.0 // Duration (seconds) ); // Pulse pattern let pulse = HapticFeedback::pulse( 0.6, // intensity 100.0, // frequency 0.3 // duration ); ``` **Pattern Types:** | Constant | Value | Description | |----------|-------|-------------| | `PATTERN_CONSTANT` | 0 | Constant intensity | | `PATTERN_PULSE` | 1 | Pulsing pattern | | `PATTERN_RAMP` | 2 | Ramping intensity | **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `vibration_intensity` | `f32` | — | Vibration intensity (0.0 to 1.0) | | `vibration_frequency` | `f32` | Hz | Vibration frequency | | `duration_seconds` | `f32` | s | Duration of feedback | | `force_feedback` | `Vector3` | N | Force feedback vector | | `pattern_type` | `u8` | — | Feedback pattern (see constants) | | `enabled` | `u8` | — | Enable/disable feedback (0 = off, 1 = on) | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `vibration(intensity, frequency, duration)` | Create vibration feedback (intensity clamped to 0-1) | | `force(force, duration)` | Create force feedback | | `pulse(intensity, frequency, duration)` | Create pulse pattern feedback | ## Force Control Node Example ```rust use horus::prelude::*; struct ForceControlNode { wrench_sub: Topic, cmd_pub: Topic, impedance_pub: Topic, target_force: f64, prev_wrench: Option, } impl Node for ForceControlNode { fn name(&self) -> &str { "ForceControl" } fn tick(&mut self) { if let Some(mut wrench) = self.wrench_sub.recv() { // Apply low-pass filter if let Some(prev) = &self.prev_wrench { wrench.filter(prev, 0.2); } self.prev_wrench = Some(wrench); // Safety check if wrench.exceeds_limits(100.0, 10.0) { // Switch to compliant mode let compliant = ImpedanceParameters::compliant(); self.impedance_pub.send(compliant); return; } // Force control to maintain target contact force let error = self.target_force - wrench.force.z; let correction = error * 0.001; // Simple P control let cmd = ForceCommand::force_only( Vector3::new(0.0, 0.0, self.target_force + correction) ); self.cmd_pub.send(cmd); } } } ``` ## ContactState Represents the phase of a contact event during force-controlled manipulation. | Variant | Value | Description | |---------|-------|-------------| | `NoContact` | 0 | No contact detected | | `InitialContact` | 1 | First moment of contact detected | | `StableContact` | 2 | Stable, sustained contact | | `ContactLoss` | 3 | Contact is being lost | | `Sliding` | 4 | Sliding contact (tangential motion) | | `Impact` | 5 | High-force impact detected | ```rust use horus::prelude::*; let contact: ContactInfo = contact_sub.recv(); match contact.state { ContactState::Impact => { // Emergency: reduce force immediately cmd_pub.send(ForceCommand::zero()); } ContactState::StableContact => { // Safe to apply task forces } _ => {} } ``` ## See Also - [Geometry Messages](/rust/api/geometry-messages) - Vector3, Point3 - [Control Messages](/rust/api/control-messages) - Motor and actuator commands --- ## Control Messages Path: /rust/api/control-messages Description: Motor control, servo, PID, trajectory, and joint command messages # Control Messages HORUS provides comprehensive message types for controlling motors, servos, and multi-joint robotic systems. All control messages are fixed-size types optimized for zero-copy shared memory transport at ~50ns latency. ## MotorCommand Direct motor control with multiple control modes. ```rust use horus::prelude::*; // Provides control::MotorCommand; // Velocity control let vel_cmd = MotorCommand::velocity(0, 1.5); // motor_id=0, 1.5 rad/s // Position control with max velocity let pos_cmd = MotorCommand::position(0, 3.14, 2.0); // motor_id=0, 3.14 rad, max 2 rad/s // Stop motor let stop_cmd = MotorCommand::stop(0); // Check validity if vel_cmd.is_valid() { println!("Target: {:.2}", vel_cmd.target); } ``` **Control Modes:** | Constant | Value | Description | |----------|-------|-------------| | `MODE_VELOCITY` | 0 | Velocity control (rad/s) | | `MODE_POSITION` | 1 | Position control (rad) | | `MODE_TORQUE` | 2 | Torque control (Nm) | | `MODE_VOLTAGE` | 3 | Direct voltage control | **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `motor_id` | `u8` | — | Motor identifier (0-255) | | `mode` | `u8` | — | Control mode (see constants above) | | `target` | `f64` | mode-dependent | rad/s (velocity), rad (position), Nm (torque), V (voltage) | | `max_velocity` | `f64` | rad/s | Velocity limit in position mode. 0.0 = no limit | | `max_acceleration` | `f64` | rad/s² | Acceleration limit. 0.0 = no limit | | `feed_forward` | `f64` | Feed-forward term | | `enable` | `u8` | Motor enabled (1=enabled, 0=disabled) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## DifferentialDriveCommand Commands for differential drive robots. ```rust use horus::prelude::*; // Provides control::DifferentialDriveCommand; // Direct wheel velocities let cmd = DifferentialDriveCommand::new(1.0, 1.2); // left=1.0, right=1.2 rad/s // From linear and angular velocities let wheel_base = 0.5; // 50cm between wheels let wheel_radius = 0.1; // 10cm wheels let cmd = DifferentialDriveCommand::from_twist( 0.5, // 0.5 m/s forward 0.2, // 0.2 rad/s rotation wheel_base, wheel_radius ); // Stop let stop = DifferentialDriveCommand::stop(); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `left_velocity` | `f64` | Left wheel velocity (rad/s) | | `right_velocity` | `f64` | Right wheel velocity (rad/s) | | `max_acceleration` | `f64` | Acceleration limit (rad/s²) | | `enable` | `u8` | Motors enabled (1=enabled, 0=disabled) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new(left, right)` | Create with wheel velocities (rad/s) | | `from_twist(linear, angular, wheel_base, wheel_radius)` | Create from linear/angular velocity | | `stop()` | Stop both motors | | `is_valid()` | Check if all values are finite | ## ServoCommand Position-controlled servo commands. ```rust use horus::prelude::*; // Provides control::ServoCommand; // Position command (radians) let cmd = ServoCommand::new(0, 1.57); // servo_id=0, 90 degrees // With specific speed (0-1) let cmd = ServoCommand::with_speed(0, 1.57, 0.3); // 30% speed // From degrees let cmd = ServoCommand::from_degrees(0, 90.0); // Disable servo (release torque) let disable = ServoCommand::disable(0); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `servo_id` | `u8` | Servo identifier | | `position` | `f32` | Target position (radians) | | `speed` | `f32` | Movement speed (0-1, 0=max speed) | | `enable` | `u8` | Torque enabled (1=enabled, 0=disabled) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## PidConfig PID controller configuration. ```rust use horus::prelude::*; // Provides control::PidConfig; // Full PID let pid = PidConfig::new(1.0, 0.1, 0.05); // Kp, Ki, Kd // P-only controller let p_only = PidConfig::proportional(2.0); // PI controller let pi = PidConfig::pi(1.5, 0.2); // PD controller let pd = PidConfig::pd(1.0, 0.1); // With limits let pid_limited = PidConfig::new(1.0, 0.1, 0.05) .with_limits(10.0, 100.0); // integral_limit, output_limit // Validation if pid.is_valid() { println!("Kp={}, Ki={}, Kd={}", pid.kp, pid.ki, pid.kd); } ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `controller_id` | `u8` | Controller identifier | | `kp` | `f64` | Proportional gain | | `ki` | `f64` | Integral gain | | `kd` | `f64` | Derivative gain | | `integral_limit` | `f64` | Integral windup limit | | `output_limit` | `f64` | Output saturation limit | | `anti_windup` | `u8` | Anti-windup enabled (1=enabled, 0=disabled) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## TrajectoryPoint Single point in a trajectory. ```rust use horus::prelude::*; // Provides control::TrajectoryPoint; // Simple 2D trajectory point let point = TrajectoryPoint::new_2d( 1.0, 2.0, // x, y position 0.5, 0.3, // vx, vy velocity 1.5 // time from start (seconds) ); // Stationary point (x, y, z) let waypoint = TrajectoryPoint::stationary(1.0, 2.0, 0.0); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `position` | `[f64; 3]` | Position [x, y, z] | | `velocity` | `[f64; 3]` | Velocity [vx, vy, vz] | | `acceleration` | `[f64; 3]` | Acceleration [ax, ay, az] | | `orientation` | `[f64; 4]` | Quaternion [x, y, z, w] | | `angular_velocity` | `[f64; 3]` | Angular velocity [wx, wy, wz] | | `time_from_start` | `f64` | Time offset (seconds) | ## JointCommand Multi-joint command for robot arms and manipulators. ```rust use horus::prelude::*; // Provides control::JointCommand; let mut cmd = JointCommand::new(); // Add position commands cmd.add_position("shoulder", 0.5)?; cmd.add_position("elbow", 1.0)?; cmd.add_position("wrist", -0.3)?; // Add velocity commands cmd.add_velocity("gripper", 0.2)?; ``` **Control Modes:** | Constant | Value | Description | |----------|-------|-------------| | `MODE_POSITION` | 0 | Position control (rad) | | `MODE_VELOCITY` | 1 | Velocity control (rad/s) | | `MODE_EFFORT` | 2 | Torque/effort control (Nm) | **Fields:** | Field | Type | Description | |-------|------|-------------| | `joint_names` | `[[u8; 32]; 16]` | Joint name strings | | `joint_count` | `u8` | Number of joints (max 16) | | `positions` | `[f64; 16]` | Position commands (rad) | | `velocities` | `[f64; 16]` | Velocity commands (rad/s) | | `efforts` | `[f64; 16]` | Effort commands (Nm) | | `modes` | `[u8; 16]` | Control mode per joint | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## Motor Control Node Example ```rust use horus::prelude::*; struct MotorDriverNode { cmd_sub: Topic, left_motor_pub: Topic, right_motor_pub: Topic, wheel_base: f64, wheel_radius: f64, } impl Node for MotorDriverNode { fn name(&self) -> &str { "MotorDriver" } fn tick(&mut self) { if let Some(cmd) = self.cmd_sub.recv() { // Convert CmdVel to differential drive let diff = DifferentialDriveCommand::from_twist( cmd.linear as f64, cmd.angular as f64, self.wheel_base, self.wheel_radius ); // Send motor velocity commands self.left_motor_pub.send(MotorCommand::velocity(0, diff.left_velocity)); self.right_motor_pub.send(MotorCommand::velocity(1, diff.right_velocity)); } } } ``` ## See Also - [Navigation Messages](/rust/api/navigation-messages) - Path and goal messages - [Sensor Messages](/rust/api/sensor-messages) - Sensor feedback data --- ## Sensor Messages Path: /rust/api/sensor-messages Description: Lidar, IMU, odometry, GPS, range sensors, and battery state # Sensor Messages HORUS provides standard sensor data formats for common robotics sensors including lidar, IMU, GPS, and battery monitoring. All sensor messages are fixed-size types optimized for zero-copy shared memory transport at ~50ns latency. ## LaserScan Laser scan data from a 2D lidar sensor. Fixed-size array (360 readings) for shared memory safety. Supports up to 360-degree scanning with 1-degree resolution. ```rust use horus::prelude::*; // Create a new laser scan let mut scan = LaserScan::new(); // Set scan parameters scan.angle_min = -std::f32::consts::PI; // -180 degrees scan.angle_max = std::f32::consts::PI; // +180 degrees scan.range_min = 0.1; // 10cm minimum scan.range_max = 30.0; // 30m maximum // Fill in range data (360 readings) for i in 0..360 { scan.ranges[i] = 2.5; // 2.5m reading at all angles } // Get angle for a specific reading let angle = scan.angle_at(90); // 90th reading println!("Angle at index 90: {:.2} rad", angle); // Check if a reading is valid if scan.is_range_valid(45) { println!("Reading at 45 is valid: {:.2}m", scan.ranges[45]); } // Get statistics let valid = scan.valid_count(); let min_dist = scan.min_range(); println!("Valid readings: {}, Min distance: {:?}", valid, min_dist); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `ranges` | `[f32; 360]` | m | Range measurements. 0.0 = invalid/no return | | `angle_min` | `f32` | rad | Start angle (typically -pi or 0) | | `angle_max` | `f32` | rad | End angle (typically pi or 2*pi) | | `range_min` | `f32` | m | Minimum valid range (readings below are noise) | | `range_max` | `f32` | m | Maximum valid range (readings above are no-return) | | `angle_increment` | `f32` | rad | Angular step between consecutive ranges | | `time_increment` | `f32` | s | Time between consecutive measurements | | `scan_time` | `f32` | s | Duration of complete scan rotation | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `sensor_msgs/msg/LaserScan` > > **Valid range check:** `range_min <= ranges[i] <= range_max` (ranges outside are invalid) > > **Typical values:** RPLiDAR A1: 360 ranges, angle_min=0, angle_max=2*pi, range_max=12m **Methods:** | Method | Description | |--------|-------------| | `new()` | Create with default parameters | | `angle_at(index)` | Get angle for a specific range index | | `is_range_valid(index)` | Check if a reading is valid | | `valid_count()` | Count valid range readings | | `min_range()` | Get minimum valid range reading | ## Imu IMU (Inertial Measurement Unit) sensor data. Provides orientation, angular velocity, and linear acceleration measurements. ```rust use horus::prelude::*; // Create new IMU message let mut imu = Imu::new(); // Set orientation from Euler angles (roll, pitch, yaw) imu.set_orientation_from_euler(0.0, 0.05, 1.57); // Slight pitch, 90° yaw // Set angular velocity [x, y, z] in rad/s imu.angular_velocity = [0.0, 0.0, 0.5]; // Rotating around Z-axis // Set linear acceleration [x, y, z] in m/s² imu.linear_acceleration = [0.0, 0.0, 9.81]; // Gravity pointing up // Check data availability if imu.has_orientation() { println!("Orientation: {:?}", imu.orientation); } // Get as Vector3 for calculations let angular_vel = imu.angular_velocity_vec(); let linear_acc = imu.linear_acceleration_vec(); println!("Angular velocity magnitude: {:.2} rad/s", angular_vel.magnitude()); // Validate data assert!(imu.is_valid()); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `orientation` | `[f64; 4]` | — | Quaternion [x, y, z, w]. Identity = [0, 0, 0, 1] | | `orientation_covariance` | `[f64; 9]` | rad² | 3x3 row-major. Set first element to -1 if no orientation data | | `angular_velocity` | `[f64; 3]` | rad/s | Gyroscope [roll_rate, pitch_rate, yaw_rate] | | `angular_velocity_covariance` | `[f64; 9]` | (rad/s)² | 3x3 row-major covariance | | `linear_acceleration` | `[f64; 3]` | m/s² | Accelerometer [x, y, z]. Includes gravity (~9.81 on Z when level) | | `linear_acceleration_covariance` | `[f64; 9]` | (m/s²)² | 3x3 row-major covariance | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `sensor_msgs/msg/Imu` > > **Gravity convention:** `linear_acceleration` includes gravity. A stationary IMU reads ~[0, 0, 9.81]. Subtract gravity for motion-only acceleration. > > **Covariance = -1:** Set the first element of a covariance matrix to -1.0 to indicate "no data available" for that measurement. **Methods:** | Method | Description | |--------|-------------| | `new()` | Create new IMU message | | `set_orientation_from_euler(roll, pitch, yaw)` | Set orientation from Euler angles | | `has_orientation()` | Check if orientation data available | | `is_valid()` | Check if all values are finite | | `angular_velocity_vec()` | Get angular velocity as `Vector3` | | `linear_acceleration_vec()` | Get linear acceleration as `Vector3` | ## Odometry Odometry data combining pose and velocity. Typically computed from wheel encoders or visual odometry, provides the robot's estimated position and velocity. ```rust use horus::prelude::*; // Create odometry message let mut odom = Odometry::new(); // Set coordinate frames odom.set_frames("odom", "base_link"); // Update with current pose and velocity let pose = Pose2D::new(5.0, 3.0, 0.785); // x, y, theta let twist = Twist::new_2d(0.5, 0.1); // linear, angular odom.update(pose, twist); // Access pose and velocity println!("Position: ({:.2}, {:.2})", odom.pose.x, odom.pose.y); println!("Velocity: {:.2} m/s", odom.twist.linear[0]); // Validate assert!(odom.is_valid()); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `pose` | `Pose2D` | m, rad | Current position (x, y) and heading (theta) | | `twist` | `Twist` | m/s, rad/s | Current linear and angular velocity | | `pose_covariance` | `[f64; 36]` | mixed | 6x6 row-major: [x, y, z, roll, pitch, yaw] covariance | | `twist_covariance` | `[f64; 36]` | mixed | 6x6 row-major velocity covariance | | `frame_id` | `[u8; 32]` | — | Reference frame name (e.g., "odom") | | `child_frame_id` | `[u8; 32]` | — | Body frame name (e.g., "base_link") | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `nav_msgs/msg/Odometry` **Methods:** | Method | Description | |--------|-------------| | `new()` | Create new odometry message | | `set_frames(frame, child_frame)` | Set coordinate frame names | | `update(pose, twist)` | Update pose and velocity with timestamp | | `is_valid()` | Check if pose and twist are valid | ## RangeSensor Single-point range sensor data (ultrasonic, infrared). ```rust use horus::prelude::*; // Create ultrasonic range reading let ultrasonic = RangeSensor::new(RangeSensor::ULTRASONIC, 1.5); // 1.5m reading // Create infrared range reading let ir = RangeSensor::new(RangeSensor::INFRARED, 0.3); // 30cm reading // Check if reading is valid (within sensor limits) if ultrasonic.is_valid() { println!("Distance: {:.2}m", ultrasonic.range); } // Access sensor parameters println!("FOV: {:.2} rad", ultrasonic.field_of_view); println!("Range: {:.2} - {:.2}m", ultrasonic.min_range, ultrasonic.max_range); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `sensor_type` | `u8` | — | 0 = ultrasonic, 1 = infrared | | `field_of_view` | `f32` | rad | Sensor field of view cone angle | | `min_range` | `f32` | m | Minimum valid range | | `max_range` | `f32` | m | Maximum valid range | | `range` | `f32` | m | Current range reading | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `sensor_msgs/msg/Range` **Constants:** | Constant | Value | Description | |----------|-------|-------------| | `RangeSensor::ULTRASONIC` | 0 | Ultrasonic sensor | | `RangeSensor::INFRARED` | 1 | Infrared sensor | **Methods:** | Method | Description | |--------|-------------| | `new(sensor_type, range)` | Create with sensor type and reading | | `is_valid()` | Check if reading is within sensor limits | ## NavSatFix GPS/GNSS position data. Standard GNSS position data from GPS, GLONASS, Galileo, or other satellite navigation systems. ```rust use horus::prelude::*; // Create GPS fix from coordinates let fix = NavSatFix::from_coordinates( 37.7749, // Latitude (positive = North) -122.4194, // Longitude (positive = East) 10.5 // Altitude in meters ); // Check fix status if fix.has_fix() { println!("GPS Fix acquired!"); println!("Position: {:.6}°N, {:.6}°E", fix.latitude, fix.longitude); println!("Altitude: {:.1}m", fix.altitude); println!("Satellites: {}", fix.satellites_visible); } // Get accuracy estimate let accuracy = fix.horizontal_accuracy(); println!("Horizontal accuracy: ±{:.1}m", accuracy); // Calculate distance to another position let destination = NavSatFix::from_coordinates(37.8044, -122.2712, 0.0); let distance = fix.distance_to(&destination); println!("Distance to destination: {:.0}m", distance); // Validate coordinates assert!(fix.is_valid()); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `latitude` | `f64` | deg | Latitude (+ = North, - = South). WGS84 | | `longitude` | `f64` | deg | Longitude (+ = East, - = West). WGS84 | | `altitude` | `f64` | m | Altitude above WGS84 ellipsoid | | `position_covariance` | `[f64; 9]` | m² | 3x3 position covariance (ENU frame) | | `position_covariance_type` | `u8` | — | Covariance type (see constants) | | `status` | `u8` | — | Fix status (see constants) | | `satellites_visible` | `u16` | — | Satellite count | | `hdop` | `f32` | — | Horizontal dilution of precision (lower = better, <2 = good) | | `vdop` | `f32` | — | Vertical dilution of precision | | `speed` | `f32` | m/s | Ground speed | | `heading` | `f32` | deg | Course over ground (0 = North, 90 = East) | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `sensor_msgs/msg/NavSatFix` **Status Constants:** | Constant | Value | Description | |----------|-------|-------------| | `STATUS_NO_FIX` | 0 | No GPS fix | | `STATUS_FIX` | 1 | Standard GPS fix | | `STATUS_SBAS_FIX` | 2 | SBAS-augmented fix | | `STATUS_GBAS_FIX` | 3 | GBAS-augmented fix | **Covariance Type Constants:** | Constant | Value | Description | |----------|-------|-------------| | `COVARIANCE_TYPE_UNKNOWN` | 0 | Unknown covariance | | `COVARIANCE_TYPE_APPROXIMATED` | 1 | Approximated covariance | | `COVARIANCE_TYPE_DIAGONAL_KNOWN` | 2 | Diagonal elements known | | `COVARIANCE_TYPE_KNOWN` | 3 | Full covariance matrix known | **Methods:** | Method | Description | |--------|-------------| | `new()` | Create empty fix | | `from_coordinates(lat, lon, alt)` | Create from coordinates | | `has_fix()` | Check if valid GPS fix | | `is_valid()` | Check if coordinates are valid | | `horizontal_accuracy()` | Estimate accuracy from HDOP | | `distance_to(&other)` | Calculate distance using Haversine formula | ## BatteryState Battery status monitoring. ```rust use horus::prelude::*; // Create battery state let mut battery = BatteryState::new(12.6, 85.0); // 12.6V, 85% // Set additional fields battery.current = -2.5; // Discharging at 2.5A battery.temperature = 28.0; battery.power_supply_status = BatteryState::STATUS_DISCHARGING; // Check battery level if battery.is_low(20.0) { println!("Battery low!"); } if battery.is_critical() { println!("Battery critical (below 10%)!"); } // Estimate remaining time if let Some(remaining) = battery.time_remaining() { println!("Estimated time remaining: {:.0} seconds", remaining); } println!("Voltage: {:.2}V", battery.voltage); println!("Charge: {:.0}%", battery.percentage); println!("Temperature: {:.1}°C", battery.temperature); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `voltage` | `f32` | Voltage in volts | | `current` | `f32` | Current in amperes (- = discharging) | | `charge` | `f32` | Charge in amp-hours (NaN if unknown) | | `capacity` | `f32` | Capacity in amp-hours (NaN if unknown) | | `percentage` | `f32` | Charge percentage (0-100) | | `power_supply_status` | `u8` | Status (see constants) | | `temperature` | `f32` | Temperature in Celsius | | `cell_voltages` | `[f32; 16]` | Individual cell voltages | | `cell_count` | `u8` | Number of valid cell readings | | `timestamp_ns` | `u64` | Nanoseconds since epoch | **Status Constants:** | Constant | Value | Description | |----------|-------|-------------| | `STATUS_UNKNOWN` | 0 | Unknown status | | `STATUS_CHARGING` | 1 | Charging | | `STATUS_DISCHARGING` | 2 | Discharging | | `STATUS_FULL` | 3 | Fully charged | **Methods:** | Method | Description | |--------|-------------| | `new(voltage, percentage)` | Create new battery state | | `is_low(threshold)` | Check if below threshold % | | `is_critical()` | Check if below 10% | | `time_remaining()` | Estimate remaining time (seconds) | ## Sensor Fusion Example ```rust use horus::prelude::*; struct SensorFusionNode { imu_sub: Topic, odom_sub: Topic, gps_sub: Topic, fused_pose_pub: Topic, // Extended Kalman Filter state ekf_state: [f64; 6], // [x, y, theta, vx, vy, omega] } impl Node for SensorFusionNode { fn name(&self) -> &str { "SensorFusion" } fn tick(&mut self) { // Process IMU at highest rate if let Some(imu) = self.imu_sub.recv() { if imu.is_valid() { // Use angular velocity for heading prediction let omega = imu.angular_velocity[2]; self.predict_state(omega); } } // Process odometry if let Some(odom) = self.odom_sub.recv() { if odom.is_valid() { // Update with wheel odometry self.update_odometry(&odom); } } // Process GPS (lower rate, absolute position) if let Some(gps) = self.gps_sub.recv() { if gps.has_fix() && gps.is_valid() { // Update with GPS (when available) self.update_gps(&gps); } } // Publish fused pose let pose = Pose2D::new( self.ekf_state[0], self.ekf_state[1], self.ekf_state[2] ); self.fused_pose_pub.send(pose); } } impl SensorFusionNode { fn predict_state(&mut self, omega: f64) { // EKF prediction step using IMU let dt = 0.01; // 100Hz self.ekf_state[2] += omega * dt; } fn update_odometry(&mut self, odom: &Odometry) { // EKF update with odometry measurement // ... implementation } fn update_gps(&mut self, gps: &NavSatFix) { // EKF update with GPS measurement // ... implementation } } ``` ## See Also - [Geometry Messages](/rust/api/geometry-messages) - Pose2D, Twist, TransformStamped - [Navigation Messages](/rust/api/navigation-messages) - Goals, paths, occupancy grids - [Perception Messages](/rust/api/perception-messages) - Point clouds, depth data --- ## ML & Segmentation Path: /rust/api/ml-messages Description: Segmentation masks and ML inference output types # ML & Segmentation The original ML message types were removed in 0.1.10. For ML inference pipelines, use: - **Model I/O**: [Tensor](/rust/api/tensor) and [TensorPool](/rust/api/tensor-pool) for zero-copy tensor transport - **Detection results**: [Detection / Detection3D](/rust/api/vision-messages) with bounding boxes - **Custom outputs**: [GenericMessage](/rust/api/generic-message) for cross-language data - **Segmentation**: `SegmentationMask` (below) ## SegmentationMask Output type for semantic, instance, and panoptic segmentation models. Fixed-size header (64 bytes) — the pixel data follows in shared memory. ```rust use horus::prelude::*; // Semantic segmentation (e.g., from DeepLab — 21 classes) let mask = SegmentationMask::semantic(640, 480, 21) .with_frame_id("camera_front") .with_timestamp(1234567890); // Instance segmentation (e.g., from Mask R-CNN) let mask = SegmentationMask::instance(640, 480); // Panoptic segmentation (e.g., from Panoptic-FPN — 80 classes) let mask = SegmentationMask::panoptic(640, 480, 80); // Send via topic let topic: Topic = Topic::new("segmentation")?; topic.send(mask); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `width` | `u32` | Mask width in pixels | | `height` | `u32` | Mask height in pixels | | `num_classes` | `u32` | Number of semantic classes | | `mask_type` | `u32` | 0=semantic, 1=instance, 2=panoptic | | `timestamp_ns` | `u64` | Nanoseconds since epoch | | `seq` | `u64` | Sequence number | | `frame_id` | `[u8; 32]` | Camera frame identifier | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `semantic(w, h, num_classes)` | `SegmentationMask` | Create semantic mask header | | `instance(w, h)` | `SegmentationMask` | Create instance mask header | | `panoptic(w, h, num_classes)` | `SegmentationMask` | Create panoptic mask header | | `with_frame_id(id)` | `Self` | Set frame ID | | `with_timestamp(ts)` | `Self` | Set timestamp | | `frame_id()` | `&str` | Get frame ID as string | | `data_size()` | `usize` | Mask data size in bytes (w * h) | ## See Also - [TensorPool API](/rust/api/tensor-pool) — Zero-copy tensor memory management - [Vision Messages](/rust/api/vision-messages) — Detection and bounding box types - [Perception Messages](/rust/api/perception-messages) — Point cloud and depth sensing --- ## Navigation Messages Path: /rust/api/navigation-messages Description: Path planning, goals, waypoints, occupancy grids, and cost maps # Navigation Messages HORUS provides message types for autonomous navigation, path planning, mapping, and localization systems. **Re-exported types** (available via `use horus::prelude::*`): `NavGoal`, `NavPath`, `PathPlan`, `OccupancyGrid`, `CostMap`. **Non-re-exported types** (require direct import): `GoalStatus`, `GoalResult`, `Waypoint`, `VelocityObstacle`, `VelocityObstacles` — import from `horus_library::messages::navigation::*`. ## NavGoal Navigation goal specification with tolerance and timeout. ```rust use horus::prelude::*; // Create navigation goal let target = Pose2D::new(5.0, 3.0, 1.57); // x, y, theta let goal = NavGoal::new(target, 0.1, 0.05); // 10cm position, 0.05rad angle tolerance // With timeout and priority let goal = NavGoal::new(target, 0.1, 0.05) .with_timeout(30.0) // 30 second timeout .with_priority(0); // Highest priority // Check if goal reached let current_pose = Pose2D::new(5.05, 3.02, 1.55); if goal.is_reached(¤t_pose) { println!("Goal reached!"); } // Check position and orientation separately if goal.is_position_reached(¤t_pose) { println!("Position reached, adjusting orientation..."); } if goal.is_orientation_reached(¤t_pose) { println!("Orientation reached"); } ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `target_pose` | `Pose2D` | m, rad | Target pose to reach | | `tolerance_position` | `f64` | m | Position tolerance | | `tolerance_angle` | `f64` | rad | Orientation tolerance | | `timeout_seconds` | `f64` | s | Maximum time (0 = no limit) | | `priority` | `u8` | — | Goal priority (0 = highest) | | `goal_id` | `u32` | — | Unique goal identifier | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `nav2_msgs/action/NavigateToPose` **Methods:** | Method | Description | |--------|-------------| | `new(target_pose, tolerance_position, tolerance_angle)` | Create a new navigation goal | | `with_timeout(seconds)` | Set timeout (builder pattern) | | `with_priority(priority)` | Set priority (builder pattern) | | `is_position_reached(¤t_pose)` | Check if position is within tolerance | | `is_orientation_reached(¤t_pose)` | Check if orientation is within tolerance | | `is_reached(¤t_pose)` | Check if both position and orientation are reached | ## GoalStatus Goal execution status enumeration. > **Note:** `GoalStatus` is not re-exported in the prelude. Import directly: `use horus_library::messages::navigation::GoalStatus;` ```rust use horus_library::messages::navigation::GoalStatus; let status = GoalStatus::Active; match status { GoalStatus::Pending => println!("Waiting to start"), GoalStatus::Active => println!("Moving to goal"), GoalStatus::Succeeded => println!("Goal reached!"), GoalStatus::Aborted => println!("Navigation failed"), GoalStatus::Cancelled => println!("Goal cancelled by user"), GoalStatus::Preempted => println!("Higher priority goal received"), GoalStatus::TimedOut => println!("Goal timed out"), } ``` **Status Values:** | Status | Value | Description | |--------|-------|-------------| | `Pending` | 0 | Goal pending execution (default) | | `Active` | 1 | Actively pursuing goal | | `Succeeded` | 2 | Goal reached successfully | | `Aborted` | 3 | Navigation aborted (error) | | `Cancelled` | 4 | Cancelled by user | | `Preempted` | 5 | Preempted by higher priority | | `TimedOut` | 6 | Goal timed out | ## GoalResult Goal status feedback with progress information. > **Note:** `GoalResult` is not re-exported in the prelude. Import directly: `use horus_library::messages::navigation::GoalResult;` ```rust use horus_library::messages::navigation::{GoalResult, GoalStatus}; // Create success result let result = GoalResult::new(42, GoalStatus::Succeeded); // Create failure result with error let error_result = GoalResult::new(42, GoalStatus::Aborted) .with_error("Obstacle blocking path"); // Update progress let mut in_progress = GoalResult::new(42, GoalStatus::Active); in_progress.distance_to_goal = 2.5; // 2.5m remaining in_progress.eta_seconds = 5.0; // 5s estimated in_progress.progress = 0.75; // 75% complete println!("Goal {}: status={}, {:.1}m to go, ETA {:.1}s", in_progress.goal_id, in_progress.status, in_progress.distance_to_goal, in_progress.eta_seconds); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `goal_id` | `u32` | — | Goal identifier | | `status` | `u8` | — | Current status (use `GoalStatus as u8` to set) | | `distance_to_goal` | `f64` | m | Distance remaining | | `eta_seconds` | `f64` | s | Estimated time to arrive | | `progress` | `f32` | — | Progress (0.0 to 1.0) | | `error_message` | `[u8; 64]` | — | Error message if failed | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new(goal_id, status)` | Create a new result (takes `GoalStatus`, stores as `u8`) | | `with_error(message)` | Set error message string (builder pattern) | ## Waypoint Single waypoint in a navigation path. > **Note:** `Waypoint` is not re-exported in the prelude. Import directly: `use horus_library::messages::navigation::Waypoint;` ```rust use horus::prelude::*; use horus_library::messages::navigation::Waypoint; // Simple waypoint let wp = Waypoint::new(Pose2D::new(1.0, 2.0, 0.0)); // Waypoint with velocity profile let wp = Waypoint::new(Pose2D::new(1.0, 2.0, 0.0)) .with_velocity(Twist::new_2d(0.5, 0.0)); // 0.5 m/s forward // Waypoint requiring stop (e.g., for pickup) let stop_wp = Waypoint::new(Pose2D::new(3.0, 4.0, 1.57)) .with_stop(); // Access properties println!("Position: ({:.1}, {:.1})", wp.pose.x, wp.pose.y); println!("Curvature: {:.3}", wp.curvature); println!("Stop required: {}", wp.stop_required); // 0 = no, 1 = yes ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `pose` | `Pose2D` | m, rad | Waypoint pose (x, y, theta) | | `velocity` | `Twist` | m/s, rad/s | Desired velocity at this point | | `time_from_start` | `f64` | s | Time from path start | | `curvature` | `f32` | 1/m | Path curvature (1/radius) | | `stop_required` | `u8` | — | Whether to stop at waypoint (0 = no, 1 = yes) | **Methods:** | Method | Description | |--------|-------------| | `new(pose)` | Create a new waypoint with default velocity | | `with_velocity(twist)` | Set desired velocity (builder pattern) | | `with_stop()` | Mark as requiring stop, sets velocity to zero | ## NavPath Navigation path with up to 256 waypoints. ```rust use horus::prelude::*; use horus_library::messages::navigation::Waypoint; // Create empty path let mut path = NavPath::new(); // Add waypoints path.add_waypoint(Waypoint::new(Pose2D::new(0.0, 0.0, 0.0)))?; path.add_waypoint(Waypoint::new(Pose2D::new(1.0, 0.0, 0.0)))?; path.add_waypoint(Waypoint::new(Pose2D::new(2.0, 1.0, 0.785)))?; // Get path info println!("Waypoints: {}", path.waypoint_count); println!("Total length: {:.2}m", path.total_length); // Get valid waypoints slice let waypoints = path.waypoints(); // Find closest waypoint to current position let current = Pose2D::new(1.2, 0.3, 0.0); if let Some(idx) = path.closest_waypoint_index(¤t) { println!("Closest waypoint: {}", idx); } // Calculate progress along path let progress = path.calculate_progress(¤t); println!("Path progress: {:.0}%", progress * 100.0); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `waypoints` | `[Waypoint; 256]` | — | Array of waypoints | | `waypoint_count` | `u16` | — | Number of valid waypoints | | `total_length` | `f64` | m | Total path length | | `duration_seconds` | `f64` | s | Estimated completion time | | `frame_id` | `[u8; 32]` | — | Coordinate frame | | `algorithm` | `[u8; 32]` | — | Planning algorithm used | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `nav_msgs/msg/Path` **Methods:** | Method | Description | |--------|-------------| | `new()` | Create a new empty path | | `add_waypoint(waypoint)` | Add a waypoint (returns `Result`, max 256) | | `waypoints()` | Get slice of valid waypoints | | `closest_waypoint_index(&pose)` | Find index of closest waypoint | | `calculate_progress(&pose)` | Calculate progress along path (0.0 to 1.0) | ## PathPlan Fixed-size path plan for zero-copy IPC transfer. Stores up to 256 waypoints as packed `[x, y, theta]` f32 values. ```rust use horus::prelude::*; // Create path plan from waypoints let waypoints = &[ [0.0f32, 0.0, 0.0], // [x, y, theta] [1.0, 0.0, 0.0], [2.0, 0.5, 0.5], [3.0, 1.0, 0.785], ]; let goal = [3.0f32, 1.0, 0.785]; let plan = PathPlan::from_waypoints(waypoints, goal); // Or build incrementally let mut plan = PathPlan::new(); plan.add_waypoint(0.0, 0.0, 0.0); plan.add_waypoint(1.0, 0.5, 0.2); plan.goal_pose = [1.0, 0.5, 0.2]; println!("Path has {} waypoints", plan.waypoint_count); println!("Empty: {}", plan.is_empty()); // Access individual waypoints if let Some(wp) = plan.get_waypoint(0) { println!("First waypoint: x={}, y={}, theta={}", wp[0], wp[1], wp[2]); } ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `waypoint_data` | `[f32; 768]` | m, rad | Packed waypoint data (256 x 3 floats: x, y, theta) | | `goal_pose` | `[f32; 3]` | m, rad | Goal pose [x, y, theta] | | `waypoint_count` | `u16` | — | Number of valid waypoints | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | **Methods:** | Method | Description | |--------|-------------| | `new()` | Create a new empty path plan | | `from_waypoints(waypoints, goal)` | Create from a slice of `[f32; 3]` waypoints | | `add_waypoint(x, y, theta)` | Add a waypoint (returns `bool`, max 256) | | `get_waypoint(index)` | Get waypoint at index as `Option<[f32; 3]>` | | `is_empty()` | Check if path has no waypoints | ## OccupancyGrid 2D occupancy grid map for navigation. Uses `Vec` data (Serde-based, variable-size). ```rust use horus::prelude::*; // Create 10m x 10m map at 5cm resolution let origin = Pose2D::origin(); let mut grid = OccupancyGrid::new( 200, // width (200 * 0.05 = 10m) 200, // height 0.05, // resolution (5cm per cell) origin ); // Set occupancy (-1=unknown, 0=free, 100=occupied) grid.set_occupancy(100, 100, 0); // Free cell grid.set_occupancy(150, 150, 100); // Occupied cell (obstacle) // World to grid coordinate conversion if let Some((gx, gy)) = grid.world_to_grid(5.0, 5.0) { println!("World (5.0, 5.0) -> Grid ({}, {})", gx, gy); } // Grid to world coordinate conversion if let Some((x, y)) = grid.grid_to_world(100, 100) { println!("Grid (100, 100) -> World ({:.2}, {:.2})", x, y); } // Check cell status let test_x = 7.5; let test_y = 7.5; if grid.is_free(test_x, test_y) { println!("({}, {}) is free", test_x, test_y); } else if grid.is_occupied(test_x, test_y) { println!("({}, {}) is occupied", test_x, test_y); } // Get occupancy value if let Some((gx, gy)) = grid.world_to_grid(test_x, test_y) { if let Some(value) = grid.get_occupancy(gx, gy) { println!("Occupancy: {}", value); } } ``` **Occupancy Values:** | Value | Meaning | |-------|---------| | `-1` | Unknown | | `0` | Free | | `1-49` | Probably free | | `50-99` | Probably occupied | | `100` | Occupied | **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `resolution` | `f32` | m/cell | Meters per pixel | | `width` | `u32` | cells | Map width in pixels | | `height` | `u32` | cells | Map height in pixels | | `origin` | `Pose2D` | m, rad | Map origin (bottom-left) | | `data` | `Vec` | — | Occupancy values (-1 to 100) | | `frame_id` | `[u8; 32]` | — | Coordinate frame | | `metadata` | `[u8; 64]` | — | Map metadata | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | > **ROS2 equivalent:** `nav_msgs/msg/OccupancyGrid` **Methods:** | Method | Description | |--------|-------------| | `new(width, height, resolution, origin)` | Create a new grid (initialized to unknown) | | `world_to_grid(x, y)` | Convert world coordinates to grid indices | | `grid_to_world(grid_x, grid_y)` | Convert grid indices to world coordinates (cell center) | | `get_occupancy(grid_x, grid_y)` | Get occupancy value at grid coordinates | | `set_occupancy(grid_x, grid_y, value)` | Set occupancy value (clamped to -1..100) | | `is_free(x, y)` | Check if world point is free (occupancy 0-49) | | `is_occupied(x, y)` | Check if world point is occupied (occupancy >= 50) | ## CostMap Navigation cost map with obstacle inflation. Uses `Vec` cost data (Serde-based, variable-size). ```rust use horus::prelude::*; // Create occupancy grid first let grid = OccupancyGrid::new(200, 200, 0.05, Pose2D::origin()); // Create costmap with inflation radius let costmap = CostMap::from_occupancy_grid(grid, 0.55); // 55cm inflation // Get cost at world coordinates (0-255, 253=lethal) if let Some(cost) = costmap.cost(5.0, 5.0) { if cost >= costmap.lethal_cost { println!("Position is in obstacle!"); } else { println!("Cost: {}", cost); } } // Access underlying grid println!("Map size: {}x{}", costmap.occupancy_grid.width, costmap.occupancy_grid.height); ``` **Cost Values:** | Value | Meaning | |-------|---------| | `0` | Free space | | `1-252` | Increasing cost (near obstacles) | | `253` | Lethal (default lethal_cost) | | `254-255` | Reserved | **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `occupancy_grid` | `OccupancyGrid` | — | Base occupancy map | | `costs` | `Vec` | — | Cost values (0-255) | | `inflation_radius` | `f32` | m | Inflation radius | | `cost_scaling_factor` | `f32` | — | Cost decay factor | | `lethal_cost` | `u8` | — | Lethal obstacle threshold (default 253) | > **ROS2 equivalent:** `nav2_msgs/msg/Costmap` **Methods:** | Method | Description | |--------|-------------| | `from_occupancy_grid(grid, inflation_radius)` | Create costmap from occupancy grid with inflation | | `cost(x, y)` | Get cost at world coordinates (returns lethal for out-of-bounds) | ## VelocityObstacle Dynamic obstacle for velocity-based avoidance. > **Note:** `VelocityObstacle` is not re-exported in the prelude. Import directly: `use horus_library::messages::navigation::VelocityObstacle;` ```rust use horus_library::messages::navigation::VelocityObstacle; let obstacle = VelocityObstacle { position: [3.0, 2.0], // [x, y] velocity: [0.5, 0.0], // Moving at 0.5 m/s in x radius: 0.3, // 30cm radius time_horizon: 5.0, // 5 second prediction obstacle_id: 1, }; println!("Obstacle {} at ({:.1}, {:.1}) moving at ({:.1}, {:.1})", obstacle.obstacle_id, obstacle.position[0], obstacle.position[1], obstacle.velocity[0], obstacle.velocity[1]); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `position` | `[f64; 2]` | m | Obstacle position [x, y] | | `velocity` | `[f64; 2]` | m/s | Obstacle velocity [vx, vy] | | `radius` | `f32` | m | Obstacle radius | | `time_horizon` | `f32` | s | Collision prediction horizon | | `obstacle_id` | `u32` | — | Tracking ID | ## VelocityObstacles Array of velocity obstacles (max 32). > **Note:** `VelocityObstacles` is not re-exported in the prelude. Import directly: `use horus_library::messages::navigation::VelocityObstacles;` ```rust use horus_library::messages::navigation::{VelocityObstacles, VelocityObstacle}; let mut obstacles = VelocityObstacles::default(); obstacles.obstacles[0] = VelocityObstacle { position: [2.0, 1.0], velocity: [0.3, 0.1], radius: 0.25, time_horizon: 3.0, obstacle_id: 1, }; obstacles.count = 1; println!("Tracking {} dynamic obstacles", obstacles.count); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `obstacles` | `[VelocityObstacle; 32]` | — | Obstacle array | | `count` | `u8` | — | Number of valid obstacles | | `timestamp_ns` | `u64` | ns | Nanoseconds since epoch | ## Navigation Node Example ```rust use horus::prelude::*; use horus_library::messages::navigation::{GoalResult, GoalStatus, Waypoint}; struct NavigationNode { goal_sub: Topic, odom_sub: Topic, map_sub: Topic, path_pub: Topic, result_pub: Topic, current_goal: Option, current_path: Option, } impl Node for NavigationNode { fn name(&self) -> &str { "Navigation" } fn tick(&mut self) { // Check for new goals if let Some(goal) = self.goal_sub.recv() { self.current_goal = Some(goal); // Plan path to goal if let Some(map) = self.map_sub.recv() { let path = self.plan_path(&goal, &map); self.path_pub.send(path); self.current_path = Some(path); } } // Check goal progress if let (Some(goal), Some(odom)) = (&self.current_goal, self.odom_sub.recv()) { let current_pose = odom.pose; // Odometry.pose is Pose2D if goal.is_reached(¤t_pose) { let result = GoalResult::new(goal.goal_id, GoalStatus::Succeeded); self.result_pub.send(result); self.current_goal = None; } else { let mut result = GoalResult::new(goal.goal_id, GoalStatus::Active); result.distance_to_goal = goal.target_pose.distance_to(¤t_pose); if let Some(path) = &self.current_path { result.progress = path.calculate_progress(¤t_pose); } self.result_pub.send(result); } } } } impl NavigationNode { fn plan_path(&self, _goal: &NavGoal, _map: &OccupancyGrid) -> NavPath { // Path planning implementation (A*, RRT*, etc.) NavPath::new() } } ``` ## GoalStatus Status of a navigation or action goal. Used by `GoalResult` and action servers. | Variant | Value | Description | |---------|-------|-------------| | `Pending` | 0 | Goal is queued, waiting to start | | `Active` | 1 | Goal is actively being pursued | | `Succeeded` | 2 | Goal was successfully reached | | `Aborted` | 3 | Goal was aborted due to error | | `Cancelled` | 4 | Goal was cancelled by user request | | `Preempted` | 5 | Goal was preempted by a higher priority goal | | `TimedOut` | 6 | Goal exceeded its time limit | ```rust use horus::prelude::*; let result = GoalResult { status: GoalStatus::Succeeded, ..Default::default() }; match result.status { GoalStatus::Succeeded => println!("Goal reached!"), GoalStatus::Aborted | GoalStatus::TimedOut => println!("Goal failed"), _ => {} } ``` ## VelocityObstacles Collection of velocity obstacles for reactive collision avoidance (VO/RVO algorithms). Fixed-size array of up to 32 obstacles. **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `obstacles` | `[VelocityObstacle; 32]` | — | Array of velocity obstacles | | `count` | `u8` | — | Number of valid obstacles (0-32) | | `timestamp_ns` | `u64` | ns | Timestamp in nanoseconds since epoch | ```rust use horus::prelude::*; // Iterate over valid obstacles let vos: VelocityObstacles = obstacle_sub.recv(); for vo in &vos.obstacles[..vos.count as usize] { // Check if desired velocity is inside the obstacle cone } ``` ## See Also - [Geometry Messages](/rust/api/geometry-messages) - Pose2D, Twist, TransformStamped - [Sensor Messages](/rust/api/sensor-messages) - Odometry, IMU, LaserScan --- ## RuntimeParams Path: /rust/api/runtime-params Description: Dynamic runtime parameter store with typed access, validation, and persistence # RuntimeParams A typed key-value store for runtime configuration. Loads defaults from `.horus/config/params.yaml`, supports typed get/set with validation, concurrent access, and persistence to disk. ## Quick Start ```rust use horus::prelude::*; let params = RuntimeParams::new()?; // Typed get with default let speed: f64 = params.get_or("max_speed", 1.0); // Typed set params.set("max_speed", 2.0)?; // Check and iterate if params.has("pid_kp") { let keys = params.list_keys(); println!("Parameters: {:?}", keys); } // Persist to disk params.save_to_disk()?; ``` ## Creating ```rust // Load from .horus/config/params.yaml (or built-in defaults) let params = RuntimeParams::new()?; // Load from explicit file let params = RuntimeParams::new()?; params.load_from_disk(Path::new("config/robot.yaml"))?; ``` If `.horus/config/params.yaml` exists, parameters are loaded from it. Otherwise, built-in defaults are used. ## Reading Parameters ```rust // Option-based (returns None if missing or type mismatch) let speed: Option = params.get("max_speed"); // With default value let kp: f64 = params.get_or("pid_kp", 1.0); // With explicit error reporting let speed: f64 = params.get_typed("max_speed")?; // Returns HorusError if missing // Check existence if params.has("emergency_stop_distance") { // ... } ``` ## Writing Parameters ```rust // Set any serde-serializable value params.set("max_speed", 1.5_f64)?; params.set("sensor_ids", vec![1, 2, 3])?; params.set("robot_name", "atlas")?; // Optimistic locking (concurrent edit protection) let version = params.get_version("max_speed"); params.set_with_version("max_speed", 2.0, version)?; // Fails if modified since // Remove a parameter params.remove("obsolete_param"); // Reset to defaults params.reset()?; ``` ## Persistence ```rust // Save current state to .horus/config/params.yaml params.save_to_disk()?; // Load from specific file params.load_from_disk(Path::new("config/prod.yaml"))?; ``` ## Built-in Defaults These parameters are available out of the box: | Key | Default | Description | |-----|---------|-------------| | `tick_rate` | `30` | Default scheduler tick rate (Hz) | | `max_memory_mb` | `512` | Memory limit | | `max_speed` | `1.0` | Maximum linear speed (m/s) | | `max_angular_speed` | `1.0` | Maximum angular speed (rad/s) | | `acceleration_limit` | `0.5` | Acceleration limit (m/s²) | | `lidar_rate` | `10` | LiDAR scan rate (Hz) | | `camera_fps` | `30` | Camera frame rate | | `sensor_timeout_ms` | `1000` | Sensor timeout (ms) | | `emergency_stop_distance` | `0.3` | E-stop trigger distance (m) | | `collision_threshold` | `0.5` | Collision detection threshold (m) | | `pid_kp` | `1.0` | PID proportional gain | | `pid_ki` | `0.1` | PID integral gain | | `pid_kd` | `0.05` | PID derivative gain | ## Usage in Nodes ```rust use horus::prelude::*; struct MotorController { params: RuntimeParams, } impl MotorController { fn new() -> Result { Ok(Self { params: RuntimeParams::new()?, }) } } impl Node for MotorController { fn name(&self) -> &str { "motor_ctrl" } fn tick(&mut self) { let kp: f64 = self.params.get_or("pid_kp", 1.0); let max_speed: f64 = self.params.get_or("max_speed", 1.0); // ... use params in control loop } } ``` ## Python Equivalent ```python import horus p = horus.Params() kp = p["pid_kp"] # 1.0 p["pid_kp"] = 2.5 p.save() ``` See [Python Bindings — Runtime Parameters](/python/api/python-bindings#runtime-parameters) for full Python API. ## API Reference | Method | Returns | Description | |--------|---------|-------------| | `new()` | `Result` | Load from `.horus/config/params.yaml` or defaults | | `get::(key)` | `Option` | Typed get, None if missing | | `get_or::(key, default)` | `T` | Get with default | | `get_typed::(key)` | `Result` | Get with error reporting | | `set(key, value)` | `Result<()>` | Set (validates if metadata exists) | | `has(key)` | `bool` | Check existence | | `list_keys()` | `Vec` | All parameter names | | `remove(key)` | `Option` | Remove and return | | `reset()` | `Result<()>` | Reset to built-in defaults | | `save_to_disk()` | `Result<()>` | Persist to YAML | | `load_from_disk(path)` | `Result<()>` | Load from YAML file | | `get_version(key)` | `u64` | Version for optimistic locking | | `set_with_version(key, value, version)` | `Result<()>` | Set with version check | ## Validation Rules Parameters can have validation rules that are enforced on every `set()` call. Rules are defined through `ParamMetadata` and checked automatically. | Rule | Applies To | Description | |------|-----------|-------------| | `MinValue(f64)` | Numbers | Value must be >= minimum | | `MaxValue(f64)` | Numbers | Value must be <= maximum | | `Range(f64, f64)` | Numbers | Value must be within [min, max] | | `RegexPattern(String)` | Strings | Value must match regex pattern | | `Enum(Vec)` | Strings | Value must be one of the allowed values | | `MinLength(usize)` | Strings/Arrays | Minimum length/element count | | `MaxLength(usize)` | Strings/Arrays | Maximum length/element count | | `RequiredKeys(Vec)` | Objects | JSON object must contain all listed keys | When a `set()` violates a rule, it returns `Err(HorusError::Validation(...))` with a descriptive message. ```rust use horus::prelude::*; // PID gain with range validation // If metadata exists with Range(0.0, 100.0), this fails: let result = params.set("pid.kp", 150.0); // Err: "Parameter 'pid.kp' value 150 exceeds maximum 100" // Enum-validated parameter let result = params.set("mode", "turbo"); // Err: "Parameter 'mode' value 'turbo' not in allowed values: [manual, auto, standby]" ``` ## Metadata `ParamMetadata` provides descriptions, units, validation, and read-only flags for parameters. | Field | Type | Description | |-------|------|-------------| | `description` | `Option` | Human-readable description | | `unit` | `Option` | Unit of measurement (e.g., "m/s", "Hz", "rad") | | `validation` | `Vec` | Validation rules (checked on set) | | `read_only` | `bool` | If true, `set()` is rejected | ```rust // Query metadata for a parameter if let Some(meta) = params.get_metadata("max_speed") { println!("Description: {:?}", meta.description); println!("Unit: {:?}", meta.unit); println!("Read-only: {}", meta.read_only()); } ``` ## Versioned Updates (Optimistic Locking) For concurrent parameter tuning (e.g., monitor UI + code both updating the same parameter), use versioned updates to prevent lost writes: ```rust use horus::prelude::*; // Read current version let version = params.get_version("pid.kp"); // Set with version check — fails if someone else modified it since our read match params.set_with_version("pid.kp", 2.5, version) { Ok(()) => println!("Updated successfully"), Err(e) => println!("Conflict: {}", e), // Someone else changed it } ``` This implements optimistic concurrency control: 1. Read the current version with `get_version(key)` 2. Compute your new value 3. Call `set_with_version(key, value, version)` — if the version hasn't changed, the write succeeds 4. If it fails, re-read and retry ## Persistence Parameters can be saved to and loaded from YAML files: ```rust // Save all parameters to disk (uses default path) params.save_to_disk()?; // Load parameters from a specific file params.load_from_disk(Path::new("robot_config.yaml"))?; ``` The YAML format is a flat key-value map: ```yaml pid.kp: 2.5 pid.ki: 0.1 pid.kd: 0.05 max_speed: 1.0 mode: "auto" ``` ## See Also - [Python Params](/python/api/python-bindings#runtime-parameters) — Python dict-like API - [horus.toml Configuration](/concepts/horus-toml) — Project-level configuration - [Parameters CLI](/development/parameters) — `horus param get/set/list` --- ## Diagnostics Messages Path: /rust/api/diagnostics-messages Description: System monitoring, health checks, heartbeats, and error reporting # Diagnostics Messages HORUS provides message types for system monitoring, health checks, error reporting, and general diagnostics. ## Heartbeat Periodic signal indicating a node is alive and operational. ```rust use horus::prelude::*; // Provides diagnostics::Heartbeat; // Create heartbeat let mut heartbeat = Heartbeat::new("MotorController", 1); // Update for each heartbeat cycle heartbeat.update(120.5); // 120.5 seconds uptime println!("Node: {}", heartbeat.name()); println!("Sequence: {}", heartbeat.sequence); println!("Uptime: {:.1}s", heartbeat.uptime); println!("Alive: {}", heartbeat.alive); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `node_name` | `[u8; 32]` | Node name (null-terminated) | | `node_id` | `u32` | Node identifier | | `sequence` | `u64` | Heartbeat sequence number | | `alive` | `u8` | Node is responding (0 = dead, 1 = alive) | | `uptime` | `f64` | Time since startup (seconds) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## DiagnosticStatus General-purpose status reporting. ```rust use horus::prelude::*; // Provides DiagnosticStatus, StatusLevel // Create status messages let ok = DiagnosticStatus::ok("System initialized successfully"); let warning = DiagnosticStatus::warn(1001, "Battery level low") .with_component("PowerManager"); let error = DiagnosticStatus::error(2001, "Sensor communication timeout") .with_component("SensorHub"); let fatal = DiagnosticStatus::fatal(9001, "Motor driver fault - emergency stop") .with_component("MotorController"); // Access status info println!("[{:?}] {}: {}", error.level, error.component_str(), error.message_str()); ``` **StatusLevel values:** | Level | Value | Description | |-------|-------|-------------| | `Ok` | 0 | Everything is OK | | `Warn` | 1 | Warning condition | | `Error` | 2 | Error (recoverable) | | `Fatal` | 3 | Fatal error (system should stop) | **Fields:** | Field | Type | Description | |-------|------|-------------| | `level` | `u8` | Severity level (use `StatusLevel as u8` to set) | | `code` | `u32` | Component-specific error code | | `message` | `[u8; 128]` | Human-readable message | | `component` | `[u8; 32]` | Reporting component name | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## EmergencyStop Critical safety message to immediately stop all robot motion. ```rust use horus::prelude::*; // Provides diagnostics::EmergencyStop; // Engage emergency stop let estop = EmergencyStop::engage("Obstacle detected in safety zone") .with_source("SafetyController"); println!("E-STOP engaged: {}", estop.engaged); println!("Reason: {}", estop.reason_str()); // Release emergency stop let release = EmergencyStop::release(); // Allow auto-reset let mut estop_auto = EmergencyStop::engage("Soft limit exceeded"); estop_auto.auto_reset = 1; ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `engaged` | `u8` | Emergency stop is active (0 = off, 1 = on) | | `reason` | `[u8; 64]` | Stop reason | | `source` | `[u8; 32]` | Triggering source | | `auto_reset` | `u8` | Can auto-reset after clearing (0 = no, 1 = yes) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## ResourceUsage System resource utilization. ```rust use horus::prelude::*; // Provides diagnostics::ResourceUsage; let mut usage = ResourceUsage::new(); usage.cpu_percent = 45.5; usage.memory_bytes = 1024 * 1024 * 512; // 512MB usage.memory_percent = 25.0; usage.temperature = 65.5; usage.thread_count = 12; // Check thresholds if usage.is_cpu_high(80.0) { println!("Warning: High CPU usage"); } if usage.is_memory_high(90.0) { println!("Warning: High memory usage"); } if usage.is_temperature_high(80.0) { println!("Warning: High temperature"); } println!("CPU: {:.1}%, Memory: {:.1}%, Temp: {:.1}C", usage.cpu_percent, usage.memory_percent, usage.temperature); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `cpu_percent` | `f32` | CPU usage (0-100) | | `memory_bytes` | `u64` | Memory usage in bytes | | `memory_percent` | `f32` | Memory usage (0-100) | | `disk_bytes` | `u64` | Disk usage in bytes | | `disk_percent` | `f32` | Disk usage (0-100) | | `network_tx_bytes` | `u64` | Network bytes sent | | `network_rx_bytes` | `u64` | Network bytes received | | `temperature` | `f32` | System temperature (Celsius) | | `thread_count` | `u32` | Active thread count | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## DiagnosticValue Key-value pair for diagnostic reports. ```rust use horus::prelude::*; // Provides diagnostics::DiagnosticValue; // Create different value types let string_val = DiagnosticValue::string("firmware_version", "1.2.3"); let int_val = DiagnosticValue::int("error_count", 42); let float_val = DiagnosticValue::float("temperature", 65.5); let bool_val = DiagnosticValue::bool("calibrated", true); ``` **Value Type Constants:** | Constant | Value | Description | |----------|-------|-------------| | `TYPE_STRING` | 0 | String value | | `TYPE_INT` | 1 | Integer value | | `TYPE_FLOAT` | 2 | Float value | | `TYPE_BOOL` | 3 | Boolean value | **Fields:** | Field | Type | Description | |-------|------|-------------| | `key` | `[u8; 32]` | Key name | | `value` | `[u8; 64]` | Value as string | | `value_type` | `u8` | Value type hint | ## DiagnosticReport Diagnostic report with multiple key-value pairs (up to 16). ```rust use horus::prelude::*; // Provides diagnostics::{DiagnosticReport, StatusLevel}; let mut report = DiagnosticReport::new("MotorController"); // Add diagnostic values report.add_string("firmware", "2.1.0")?; report.add_int("tick_count", 15000)?; report.add_float("voltage", 24.5)?; report.add_bool("calibrated", true)?; // Set overall status report.set_level(StatusLevel::Ok); println!("Report has {} values at level {}", report.value_count, report.level); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `component` | `[u8; 32]` | Component name | | `values` | `[DiagnosticValue; 16]` | Diagnostic values | | `value_count` | `u8` | Number of valid values | | `level` | `u8` | Overall status level (use `StatusLevel as u8` to set) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## NodeState Node execution state enumeration. ```rust use horus_library::messages::diagnostics::NodeState; // Note: The prelude's NodeState is the core scheduler version. // For the POD message version, import from diagnostics directly. let state = NodeState::Running; println!("State: {}", state.as_str()); // "Running" ``` **NodeState values:** | State | Value | Description | |-------|-------|-------------| | `Idle` | 0 | Created but not started | | `Initializing` | 1 | Running initialization | | `Running` | 2 | Active and executing | | `Paused` | 3 | Temporarily suspended | | `Stopped` | 4 | Cleanly shut down | | `Error` | 5 | Error/crashed state | ## HealthStatus Node operational health status. ```rust use horus::prelude::*; // Provides diagnostics::HealthStatus; let health = HealthStatus::Healthy; println!("Health: {} ({})", health.as_str(), health.color()); // Color codes for monitor display // Healthy -> "green" // Warning -> "yellow" // Error -> "orange" // Critical -> "red" // Unknown -> "gray" ``` **HealthStatus values:** | Status | Value | Description | |--------|-------|-------------| | `Healthy` | 0 | Operating normally | | `Warning` | 1 | Degraded performance | | `Error` | 2 | Errors but running | | `Critical` | 3 | Fatal errors | | `Unknown` | 4 | No heartbeat received | ## NodeHeartbeat Node status heartbeat with health information (written to shared memory). ```rust use horus::prelude::*; // Provides NodeHeartbeat, HealthStatus use horus_library::messages::diagnostics::NodeState; // POD version (distinct from core NodeState) // Create heartbeat let mut heartbeat = NodeHeartbeat::new(NodeState::Running, HealthStatus::Healthy); heartbeat.tick_count = 15000; heartbeat.target_rate = 100; heartbeat.actual_rate = 98; heartbeat.error_count = 0; // Update timestamp heartbeat.update_timestamp(); // Check freshness (within last 5 seconds) if heartbeat.is_fresh(5) { println!("Node is alive"); } // Serialize for file writing let bytes = heartbeat.to_bytes(); // Deserialize from file if let Some(hb) = NodeHeartbeat::from_bytes(&bytes) { println!("Tick rate: {}/{} Hz", hb.actual_rate, hb.target_rate); } ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `state` | `u8` | Execution state (use `NodeState as u8` to set) | | `health` | `u8` | Health status (use `HealthStatus as u8` to set) | | `tick_count` | `u64` | Total tick count | | `target_rate` | `u32` | Target tick rate | | `actual_rate` | `u32` | Measured tick rate | | `error_count` | `u32` | Error count | | `last_tick_timestamp` | `u64` | Last tick time (unix epoch seconds) | | `heartbeat_timestamp` | `u64` | Heartbeat time (unix epoch seconds) | ## SafetyStatus Safety system status. ```rust use horus::prelude::*; // Provides diagnostics::SafetyStatus; let mut safety = SafetyStatus::new(); // SafetyStatus::new() sets good defaults (enabled=1, watchdog=1, limits=1, comms=1) // Override only if needed: safety.estop_engaged = 0; // Check if safe to operate if safety.is_safe() { println!("System is safe to operate"); } else { println!("Safety interlock active - fault code: {}", safety.fault_code); } // Set fault condition safety.set_fault(1001); println!("Mode: {}", match safety.mode { SafetyStatus::MODE_NORMAL => "Normal", SafetyStatus::MODE_REDUCED => "Reduced", SafetyStatus::MODE_SAFE_STOP => "Safe Stop", _ => "Unknown" }); // Clear faults safety.clear_faults(); ``` **Mode Constants:** | Constant | Value | Description | |----------|-------|-------------| | `MODE_NORMAL` | 0 | Normal operation | | `MODE_REDUCED` | 1 | Reduced speed/power | | `MODE_SAFE_STOP` | 2 | Safe stop engaged | **Fields:** | Field | Type | Description | |-------|------|-------------| | `enabled` | `u8` | Safety system active (0 = off, 1 = on) | | `estop_engaged` | `u8` | Emergency stop engaged (0 = no, 1 = yes) | | `watchdog_ok` | `u8` | Watchdog timer OK (0 = fault, 1 = ok) | | `limits_ok` | `u8` | All limits within bounds (0 = fault, 1 = ok) | | `comms_ok` | `u8` | Communication healthy (0 = fault, 1 = ok) | | `mode` | `u8` | Safety mode | | `fault_code` | `u32` | Fault code (0 = none) | | `timestamp_ns` | `u64` | Nanoseconds since epoch | ## Diagnostics Node Example ```rust use horus::prelude::*; struct DiagnosticsNode { status_pub: Topic, resource_pub: Topic, safety_sub: Topic, estop_pub: Topic, tick_count: u64, start_time: std::time::Instant, } impl Node for DiagnosticsNode { fn name(&self) -> &str { "Diagnostics" } fn tick(&mut self) { self.tick_count += 1; // Check safety status if let Some(safety) = self.safety_sub.recv() { if !safety.is_safe() { // Trigger emergency stop let estop = EmergencyStop::engage(&format!( "Safety fault code: {}", safety.fault_code )).with_source("DiagnosticsNode"); self.estop_pub.send(estop); // Send error status let status = DiagnosticStatus::error(safety.fault_code, "Safety system fault") .with_component("SafetyMonitor"); self.status_pub.send(status); } } // Periodic resource reporting (every 100 ticks) if self.tick_count % 100 == 0 { let mut usage = ResourceUsage::new(); // ... populate with actual system metrics ... // Check thresholds if usage.is_cpu_high(90.0) { let status = DiagnosticStatus::warn(1001, "CPU usage above 90%") .with_component("ResourceMonitor"); self.status_pub.send(status); } self.resource_pub.send(usage); } // Periodic OK status (every 1000 ticks) if self.tick_count % 1000 == 0 { let uptime = self.start_time.elapsed().as_secs_f64(); let status = DiagnosticStatus::ok(&format!("System healthy, uptime: {:.0}s", uptime)) .with_component("DiagnosticsNode"); self.status_pub.send(status); } } } ``` ## StatusLevel Severity level for diagnostic status reports. Used by `DiagnosticStatus` to indicate severity. | Variant | Value | Description | |---------|-------|-------------| | `Ok` | 0 | Everything is operating normally | | `Warn` | 1 | Warning condition (degraded but functional) | | `Error` | 2 | Error condition (recoverable) | | `Fatal` | 3 | Fatal error (system should stop) | ```rust use horus::prelude::*; let status = DiagnosticStatus::new(StatusLevel::Warn, "Battery low: 15%"); ``` ## NodeStateMsg Represents the lifecycle state of a node. Published by the scheduler for monitoring. | Variant | Value | Description | |---------|-------|-------------| | `Idle` | 0 | Node created but not yet started | | `Initializing` | 1 | Running `init()` | | `Running` | 2 | Active and executing `tick()` | | `Paused` | 3 | Temporarily suspended | | `Stopped` | 4 | Cleanly shut down | | `Error` | 5 | Error or crashed state | ```rust use horus::prelude::*; // Monitor node state transitions if let Some(state) = node_state_sub.try_recv() { match state { NodeStateMsg::Running => println!("Node is active"), NodeStateMsg::Error => println!("Node has errors!"), _ => {} } } ``` ## See Also - [Safety Monitor](/advanced/safety-monitor) - Safety monitoring features - [BlackBox Flight Recorder](/advanced/blackbox) - Event recording and crash analysis --- ## Vision Messages Path: /rust/api/vision-messages Description: Camera, image, calibration, and visual detection messages # Vision Messages HORUS provides message types for cameras, images, camera calibration, and visual detection systems. ## Image Pool-backed RAII image type with zero-copy shared memory transport. Image allocates from a global tensor pool — you don't manage memory directly. ```rust use horus::prelude::*; // Create an RGB image (width, height, encoding) — allocates from global pool let mut image = Image::new(640, 480, ImageEncoding::Rgb8)?; // Copy pixel data into the image let pixels: Vec = vec![128; 480 * 640 * 3]; image.copy_from(&pixels); // Set metadata (method chaining) image.set_frame_id("camera_front").set_timestamp_ns(1234567890); // Access image properties println!("Image: {}x{}, {:?}", image.width(), image.height(), image.encoding()); println!("Data size: {} bytes", image.data().len()); // Access individual pixel (x, y) if let Some(pixel) = image.pixel(0, 0) { println!("Pixel[0,0]: R={}, G={}, B={}", pixel[0], pixel[1], pixel[2]); } // Set a pixel value image.set_pixel(0, 0, &[255, 0, 0]); // Extract region of interest (returns raw bytes, not Image) if let Some(roi_data) = image.roi(0, 0, 100, 100) { println!("ROI data: {} bytes", roi_data.len()); } // Fill entire image with a color image.fill(&[0, 0, 0]); // Black ``` **ImageEncoding values:** | Encoding | Channels | Bytes/Pixel | Description | |----------|----------|-------------|-------------| | `Mono8` | 1 | 1 | 8-bit monochrome | | `Mono16` | 1 | 2 | 16-bit monochrome | | `Rgb8` | 3 | 3 | 8-bit RGB (default) | | `Bgr8` | 3 | 3 | 8-bit BGR (OpenCV) | | `Rgba8` | 4 | 4 | 8-bit RGBA | | `Bgra8` | 4 | 4 | 8-bit BGRA | | `Yuv422` | 2 | 2 | YUV 4:2:2 | | `Mono32F` | 1 | 4 | 32-bit float mono | | `Rgb32F` | 3 | 12 | 32-bit float RGB | | `BayerRggb8` | 1 | 1 | Bayer pattern (raw) | | `Depth16` | 1 | 2 | 16-bit depth (mm) | **ImageEncoding methods:** | Method | Returns | Description | |--------|---------|-------------| | `bytes_per_pixel()` | `u32` | Bytes per pixel for this encoding | | `is_color()` | `bool` | Whether encoding has color information | **Image methods:** Image is an RAII type — fields are private, accessed through methods. Mutation methods return `&mut Self` for chaining. | Method | Returns | Description | |--------|---------|-------------| | `new(width, height, encoding)` | `Result` | Create image (allocates from global pool) | | `width()` | `u32` | Image width in pixels | | `height()` | `u32` | Image height in pixels | | `encoding()` | `ImageEncoding` | Pixel encoding format | | `data()` | `&[u8]` | Zero-copy access to pixel data | | `data_mut()` | `&mut [u8]` | Mutable access to pixel data | | `copy_from(src)` | `&mut Self` | Copy pixel data into image | | `pixel(x, y)` | `Option<&[u8]>` | Get pixel bytes at coordinates | | `set_pixel(x, y, value)` | `&mut Self` | Set pixel value at coordinates | | `fill(value)` | `&mut Self` | Fill entire image with a value | | `roi(x, y, w, h)` | `Option>` | Extract raw bytes for a region | | `set_frame_id(id)` | `&mut Self` | Set camera frame identifier | | `set_timestamp_ns(ts)` | `&mut Self` | Set timestamp in nanoseconds | ## CompressedImage Compressed image data (JPEG, PNG, etc.). ```rust use horus::prelude::*; // Create compressed image from JPEG data let jpeg_data = std::fs::read("image.jpg").unwrap(); let compressed = CompressedImage::new("jpeg", jpeg_data); println!("Format: {}", compressed.format_str()); println!("Compressed size: {} bytes", compressed.data.len()); // Optional: set original dimensions if known let mut img = compressed; img.width = 640; img.height = 480; ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `format` | `[u8; 8]` | Compression format string (null-padded) | | `data` | `Vec` | Compressed data | | `width` | `u32` | Original width (0 if unknown) | | `height` | `u32` | Original height (0 if unknown) | | `frame_id` | `[u8; 32]` | Camera identifier | | `timestamp_ns` | `u64` | Nanoseconds since epoch | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(format, data)` | `CompressedImage` | Create from format string and data (auto-sets timestamp) | | `format_str()` | `String` | Get format as string | ## CameraInfo Camera calibration information. ```rust use horus::prelude::*; // Create camera info with intrinsics let camera = CameraInfo::new( 640, 480, // width, height 525.0, 525.0, // fx, fy 320.0, 240.0 // cx, cy (principal point) ).with_distortion_model("plumb_bob"); // Access intrinsics let (fx, fy) = camera.focal_lengths(); let (cx, cy) = camera.principal_point(); println!("Focal length: ({:.1}, {:.1})", fx, fy); println!("Principal point: ({:.1}, {:.1})", cx, cy); // Set distortion coefficients let mut camera = camera; camera.distortion_coefficients = [ -0.25, // k1 0.12, // k2 0.001, // p1 -0.001, // p2 0.0, 0.0, 0.0, 0.0 // k3-k6 ]; ``` **Camera Matrix (3x3):** ``` [fx, 0, cx] [ 0, fy, cy] [ 0, 0, 1] ``` **Projection Matrix (3x4):** ``` [fx', 0, cx', Tx] [ 0, fy', cy', Ty] [ 0, 0, 1, 0] ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `width` | `u32` | Image width in pixels | | `height` | `u32` | Image height in pixels | | `distortion_model` | `[u8; 16]` | Distortion model name (null-padded) | | `distortion_coefficients` | `[f64; 8]` | [k1, k2, p1, p2, k3, k4, k5, k6] | | `camera_matrix` | `[f64; 9]` | 3x3 intrinsic matrix (row-major) | | `rectification_matrix` | `[f64; 9]` | 3x3 rectification matrix (identity by default) | | `projection_matrix` | `[f64; 12]` | 3x4 projection matrix (row-major) | | `frame_id` | `[u8; 32]` | Camera identifier | | `timestamp_ns` | `u64` | Nanoseconds since epoch | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(width, height, fx, fy, cx, cy)` | `CameraInfo` | Create with intrinsics (auto-sets camera/projection matrices) | | `with_distortion_model(model)` | `CameraInfo` | Set distortion model name (builder) | | `focal_lengths()` | `(f64, f64)` | Get (fx, fy) from camera matrix | | `principal_point()` | `(f64, f64)` | Get (cx, cy) from camera matrix | ## RegionOfInterest Region of interest (bounding box) in an image. ```rust use horus::prelude::*; // Create ROI let roi = RegionOfInterest::new(100, 50, 200, 150); // Check if point is inside ROI if roi.contains(150, 100) { println!("Point is inside ROI"); } // Get area println!("ROI area: {} pixels", roi.area()); // Access properties println!("ROI: ({}, {}) -> {}x{}", roi.x_offset, roi.y_offset, roi.width, roi.height); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x_offset` | `u32` | X offset of region | | `y_offset` | `u32` | Y offset of region | | `width` | `u32` | Region width | | `height` | `u32` | Region height | | `do_rectify` | `bool` | Apply rectification | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(x, y, width, height)` | `RegionOfInterest` | Create a new ROI | | `contains(x, y)` | `bool` | Check if point is inside ROI | | `area()` | `u32` | Get area in pixels | ## StereoInfo Stereo camera pair information. ```rust use horus_library::messages::vision::StereoInfo; use horus::prelude::*; // Create stereo configuration let left = CameraInfo::new(640, 480, 525.0, 525.0, 320.0, 240.0); let right = CameraInfo::new(640, 480, 525.0, 525.0, 320.0, 240.0); let stereo = StereoInfo { left_camera: left, right_camera: right, baseline: 0.12, // 12cm between cameras depth_scale: 1.0, }; // Calculate depth from disparity let disparity = 64.0_f32; // pixels let depth = stereo.depth_from_disparity(disparity); println!("Disparity {} -> depth {:.2}m", disparity, depth); // Calculate disparity from depth let depth = 2.0_f32; // meters let disparity = stereo.disparity_from_depth(depth); println!("Depth {}m -> disparity {:.1}px", depth, disparity); ``` > **Note:** `StereoInfo` is not included in the convenience re-exports. Import it directly from `horus_library::messages::vision::StereoInfo`. **Fields:** | Field | Type | Description | |-------|------|-------------| | `left_camera` | `CameraInfo` | Left camera calibration | | `right_camera` | `CameraInfo` | Right camera calibration | | `baseline` | `f64` | Camera distance (meters) | | `depth_scale` | `f64` | Disparity-to-depth factor | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `depth_from_disparity(disparity: f32)` | `f32` | Calculate depth from pixel disparity (returns `INFINITY` if disparity <= 0) | | `disparity_from_depth(depth: f32)` | `f32` | Calculate disparity from depth (returns 0 if depth <= 0) | ## BoundingBox2D 2D bounding box for object detection. Fixed-size, suitable for zero-copy shared memory transport. ```rust use horus::prelude::*; // Create from top-left corner let bbox = BoundingBox2D::new(100.0, 50.0, 200.0, 150.0); // Create from center (YOLO format) let bbox = BoundingBox2D::from_center(200.0, 125.0, 200.0, 150.0); // Get properties println!("Center: ({}, {})", bbox.center_x(), bbox.center_y()); println!("Area: {} px²", bbox.area()); // Calculate IoU between two boxes let other = BoundingBox2D::new(150.0, 75.0, 200.0, 150.0); println!("IoU: {:.3}", bbox.iou(&other)); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `x` | `f32` | X of top-left corner (pixels) | | `y` | `f32` | Y of top-left corner (pixels) | | `width` | `f32` | Width (pixels) | | `height` | `f32` | Height (pixels) | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(x, y, width, height)` | `BoundingBox2D` | Create from top-left corner | | `from_center(cx, cy, width, height)` | `BoundingBox2D` | Create from center (YOLO format) | | `center_x()` | `f32` | Get center X coordinate | | `center_y()` | `f32` | Get center Y coordinate | | `area()` | `f32` | Get area | | `iou(other)` | `f32` | Intersection over Union with another box | ## Detection 2D object detection result. Fixed-size (72 bytes), suitable for zero-copy shared memory transport. ```rust use horus::prelude::*; // Create detection with class name, confidence, and bounding box coordinates let det = Detection::new("person", 0.95, 100.0, 50.0, 200.0, 300.0); println!("Detected: {} ({:.1}% confidence)", det.class_name(), det.confidence * 100.0); println!("BBox: ({}, {}) {}x{}", det.bbox.x, det.bbox.y, det.bbox.width, det.bbox.height); // Check confidence threshold if det.is_confident(0.9) { println!("High confidence detection!"); } // Create with class ID instead of name let bbox = BoundingBox2D::new(100.0, 50.0, 200.0, 300.0); let det = Detection::with_class_id(1, 0.88, bbox); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `bbox` | `BoundingBox2D` | Bounding box (x, y, width, height) | | `confidence` | `f32` | Detection confidence (0.0-1.0) | | `class_id` | `u32` | Numeric class identifier | | `class_name` | `[u8; 32]` | Class name string (null-padded, max 31 chars) | | `instance_id` | `u32` | Instance ID (for instance segmentation) | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(class_name, confidence, x, y, width, height)` | `Detection` | Create with name and bbox coordinates | | `with_class_id(class_id, confidence, bbox)` | `Detection` | Create with numeric class ID | | `set_class_name(name)` | `()` | Set class name (truncates to 31 chars) | | `class_name()` | `&str` | Get class name as string | | `is_confident(threshold)` | `bool` | Check if confidence >= threshold | ## Detection3D 3D object detection from point clouds or depth-aware models. Fixed-size (104 bytes) with velocity tracking. ```rust use horus::prelude::*; // Create 3D bounding box (center, dimensions, yaw) let bbox = BoundingBox3D::new( 5.0, 2.0, 0.5, // center (x, y, z) in meters 4.5, 2.0, 1.5, // dimensions (length, width, height) 0.1 // yaw rotation in radians ); // Create 3D detection with velocity let det = Detection3D::new("car", 0.92, bbox) .with_velocity(10.0, 5.0, 0.0); // m/s println!("Detected: {} at ({}, {}, {})", det.class_name(), det.bbox.cx, det.bbox.cy, det.bbox.cz); println!("Volume: {:.1} m³", det.bbox.volume()); ``` **BoundingBox3D fields:** | Field | Type | Description | |-------|------|-------------| | `cx`, `cy`, `cz` | `f32` | Center coordinates (meters) | | `length`, `width`, `height` | `f32` | Dimensions (meters) | | `roll`, `pitch`, `yaw` | `f32` | Euler angles (radians) | **Detection3D fields:** | Field | Type | Description | |-------|------|-------------| | `bbox` | `BoundingBox3D` | 3D bounding box | | `confidence` | `f32` | Confidence score (0.0-1.0) | | `class_id` | `u32` | Numeric class identifier | | `class_name` | `[u8; 32]` | Class name (null-padded, max 31 chars) | | `velocity_x`, `velocity_y`, `velocity_z` | `f32` | Velocity in m/s | | `instance_id` | `u32` | Instance/tracking ID | ## Vision Processing Node Example ```rust use horus::prelude::*; struct VisionNode { image_sub: Topic, camera_info_sub: Topic, detection_pub: Topic, camera_info: Option, } impl Node for VisionNode { fn name(&self) -> &str { "VisionNode" } fn tick(&mut self) { // Update camera calibration if let Some(info) = self.camera_info_sub.recv() { self.camera_info = Some(info); } // Process images if let Some(image) = self.image_sub.recv() { // Run detection (your ML model here) let detection = Detection::new( "person", 0.95, 100.0, 50.0, 200.0, 300.0 ); self.detection_pub.send(detection); } } } ``` ## See Also - [Perception Messages](/rust/api/perception-messages) - PointCloud, DepthImage - [Message Types](/concepts/message-types) - Standard message type overview --- ## Image Path: /rust/api/image Description: Zero-copy shared memory image type for camera pipelines # Image Pool-backed RAII image type with zero-copy shared memory transport. `Image` allocates from a global tensor pool automatically — you never manage memory directly. Only a lightweight descriptor travels through topics; the pixel data stays in shared memory at ~50ns IPC latency. ## Quick Start ```rust use horus::prelude::*; // Create a 640x480 RGB image let mut img = Image::new(640, 480, ImageEncoding::Rgb8)?; // Fill with red img.fill(&[255, 0, 0]); // Set metadata (chainable) img.set_frame_id("camera_front") .set_timestamp_ns(1_700_000_000_000_000_000); // Publish on a topic — zero-copy, only the descriptor is sent let topic: Topic = Topic::new("camera.rgb")?; topic.send(&img); ``` ```rust // Receiver side let topic: Topic = Topic::new("camera.rgb")?; if let Some(img) = topic.recv() { println!("{}x{} {:?}", img.width(), img.height(), img.encoding()); println!("Frame: {}, Timestamp: {}ns", img.frame_id(), img.timestamp_ns()); // Direct pixel access — zero-copy if let Some(px) = img.pixel(0, 0) { println!("Top-left pixel: R={} G={} B={}", px[0], px[1], px[2]); } } ``` ## Pixel Access ```rust use horus::prelude::*; let mut img = Image::new(640, 480, ImageEncoding::Rgb8)?; // Read a pixel — returns None if out of bounds if let Some(pixel) = img.pixel(100, 200) { println!("R={} G={} B={}", pixel[0], pixel[1], pixel[2]); } // Write a pixel — no-op if out of bounds, chainable img.set_pixel(100, 200, &[255, 128, 0]) .set_pixel(101, 200, &[255, 128, 0]); // Fill entire image with a single color img.fill(&[0, 0, 0]); // Black // Extract a region of interest (returns raw bytes) if let Some(roi_data) = img.roi(10, 10, 100, 100) { println!("ROI: {} bytes", roi_data.len()); } ``` ## Raw Data Access ```rust use horus::prelude::*; let mut img = Image::new(640, 480, ImageEncoding::Rgb8)?; // Zero-copy read access to the underlying buffer let data: &[u8] = img.data(); println!("Total bytes: {}", data.len()); // 640 * 480 * 3 // Mutable access for bulk operations let data_mut: &mut [u8] = img.data_mut(); data_mut[0] = 255; // Set first byte directly // Copy from an external buffer let pixels: Vec = vec![128; 640 * 480 * 3]; img.copy_from(&pixels); ``` ## Camera Pipeline Example A complete camera processing node that receives images, processes them, and publishes results. ```rust use horus::prelude::*; struct CameraNode { raw_sub: Topic, processed_pub: Topic, } impl Node for CameraNode { fn name(&self) -> &str { "CameraProcessor" } fn tick(&mut self) { if let Some(raw) = self.raw_sub.recv() { // Create output image with same dimensions let mut out = Image::new( raw.width(), raw.height(), ImageEncoding::Mono8 ).unwrap(); // Convert RGB to grayscale (simple luminance) let src = raw.data(); let dst = out.data_mut(); for i in 0..((raw.width() * raw.height()) as usize) { let r = src[i * 3] as f32; let g = src[i * 3 + 1] as f32; let b = src[i * 3 + 2] as f32; dst[i] = (0.299 * r + 0.587 * g + 0.114 * b) as u8; } out.set_frame_id(raw.frame_id()) .set_timestamp_ns(raw.timestamp_ns()); self.processed_pub.send(&out); } } } ``` ## Depth Camera Example ```rust use horus::prelude::*; // Create a 16-bit depth image (values in millimeters) let mut depth = Image::new(640, 480, ImageEncoding::Depth16)?; // Each pixel is 2 bytes (u16 little-endian) let data = depth.data_mut(); let distance_mm: u16 = 1500; // 1.5 meters data[0] = (distance_mm & 0xFF) as u8; data[1] = (distance_mm >> 8) as u8; println!("Depth image: {}x{}, {} bytes/pixel, step={}", depth.width(), depth.height(), depth.encoding().bytes_per_pixel(), depth.step()); ``` ## Rust API Reference ### Constructor ```rust pub fn new(width: u32, height: u32, encoding: ImageEncoding) -> Result ``` Create an image by allocating from the global tensor pool (zero-copy SHM). **Parameters:** - `width: u32` — Image width in pixels. Must be > 0. - `height: u32` — Image height in pixels. Must be > 0. - `encoding: ImageEncoding` — Pixel format. Common values: - `ImageEncoding::Rgb8` — 3 bytes/pixel (red, green, blue) - `ImageEncoding::Rgba8` — 4 bytes/pixel (with alpha) - `ImageEncoding::Bgr8` — 3 bytes/pixel (OpenCV default order) - `ImageEncoding::Mono8` — 1 byte/pixel (grayscale) - `ImageEncoding::Mono16` — 2 bytes/pixel (16-bit grayscale) **Returns:** `Result` — `Ok(image)` or `Err(MemoryError::PoolExhausted)` if no pool slots available. **Memory:** Allocated from SHM tensor pool. Zero-copy when sent via `Topic`. The image data is NOT copied between publisher and subscriber — they share the same physical memory. **Example:** ```rust let img = Image::new(640, 480, ImageEncoding::Rgb8)?; assert_eq!(img.width(), 640); assert_eq!(img.height(), 480); assert_eq!(img.channels(), 3); ``` ### Pixel Access | Method | Returns | Description | |--------|---------|-------------| | `pixel(x, y)` | `Option<&[u8]>` | Get pixel bytes at (x, y). Returns `None` if out of bounds | | `set_pixel(x, y, value)` | `&mut Self` | Set pixel value. No-op if out of bounds. Chainable | | `fill(value)` | `&mut Self` | Fill every pixel with the same value. Chainable | | `roi(x, y, w, h)` | `Option>` | Extract a rectangular region as raw bytes | ### Metadata | Method | Returns | Description | |--------|---------|-------------| | `width()` | `u32` | Image width in pixels | | `height()` | `u32` | Image height in pixels | | `channels()` | `u32` | Number of channels (e.g., 3 for RGB) | | `encoding()` | `ImageEncoding` | Pixel encoding format | | `step()` | `u32` | Bytes per row (`width * bytes_per_pixel`) | ### Data Access | Method | Returns | Description | |--------|---------|-------------| | `data()` | `&[u8]` | Zero-copy read access to raw pixel bytes | | `data_mut()` | `&mut [u8]` | Mutable access to raw pixel bytes | | `copy_from(src)` | `&mut Self` | Copy bytes from a slice into the image buffer. Chainable | ### Frame & Timestamp | Method | Returns | Description | |--------|---------|-------------| | `set_frame_id(id)` | `&mut Self` | Set the camera frame identifier. Chainable | | `set_timestamp_ns(ts)` | `&mut Self` | Set timestamp in nanoseconds. Chainable | | `frame_id()` | `&str` | Get the camera frame identifier | | `timestamp_ns()` | `u64` | Get timestamp in nanoseconds | ### Type Info | Method | Returns | Description | |--------|---------|-------------| | `dtype()` | `TensorDtype` | Underlying tensor data type (e.g., `U8` for 8-bit encodings) | | `nbytes()` | `u64` | Total size of the pixel buffer in bytes | | `is_cpu()` | `bool` | Whether the image data resides on CPU | | `is_cuda()` | `bool` | Whether the device descriptor is set to CUDA (not currently used) | ## ImageEncoding Pixel format enum (`#[repr(u8)]`, default: `Rgb8`). | Variant | Channels | Bytes/Pixel | Description | |---------|----------|-------------|-------------| | `Mono8` | 1 | 1 | 8-bit grayscale | | `Mono16` | 1 | 2 | 16-bit grayscale | | `Rgb8` | 3 | 3 | 8-bit RGB (default) | | `Bgr8` | 3 | 3 | 8-bit BGR (OpenCV convention) | | `Rgba8` | 4 | 4 | 8-bit RGBA with alpha | | `Bgra8` | 4 | 4 | 8-bit BGRA with alpha | | `Yuv422` | 2 | 2 | YUV 4:2:2 packed | | `Mono32F` | 1 | 4 | 32-bit float grayscale | | `Rgb32F` | 3 | 12 | 32-bit float RGB | | `BayerRggb8` | 1 | 1 | Bayer RGGB raw sensor data | | `Depth16` | 1 | 2 | 16-bit depth in millimeters | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `bytes_per_pixel()` | `u32` | Number of bytes per pixel | | `channels()` | `u32` | Number of color channels | ## Python API The Python `Image` class wraps the same shared memory backend, with zero-copy interop to NumPy, PyTorch, and JAX. ### Constructor & Factories ```python import horus # Create an empty image (height, width, encoding) img = horus.Image(480, 640, encoding="rgb8") # From a NumPy array — encoding auto-detected from shape import numpy as np arr = np.zeros((480, 640, 3), dtype=np.uint8) img = horus.Image.from_numpy(arr, encoding="rgb8") # From a PyTorch tensor import torch t = torch.zeros(480, 640, 3, dtype=torch.uint8) img = horus.Image.from_torch(t, encoding="rgb8") # From raw bytes data = bytes(480 * 640 * 3) img = horus.Image.from_bytes(data, height=480, width=640, encoding="rgb8") ``` ### Zero-Copy Conversions ```python # To NumPy — zero-copy view arr = img.to_numpy() # shape: (480, 640, 3), dtype: uint8 # To PyTorch — zero-copy via DLPack tensor = img.to_torch() # To JAX — zero-copy via DLPack jax_arr = img.to_jax() ``` ### Pixel Operations ```python # Read pixel at (x, y) r, g, b = img.pixel(100, 200) # Write pixel img.set_pixel(100, 200, [255, 0, 0]) # Fill entire image img.fill([128, 128, 128]) # Extract region of interest roi_bytes = img.roi(0, 0, 100, 100) ``` ### Properties & Setters | Property | Type | Description | |----------|------|-------------| | `height` | `int` | Image height in pixels | | `width` | `int` | Image width in pixels | | `channels` | `int` | Number of channels | | `encoding` | `str` | Encoding name (e.g., `"rgb8"`) | | `dtype` | `str` | Tensor data type | | `nbytes` | `int` | Total buffer size in bytes | | `step` | `int` | Bytes per row | | `frame_id` | `str` | Camera frame identifier | | `timestamp_ns` | `int` | Timestamp in nanoseconds | ```python img.set_frame_id("camera_front") img.set_timestamp_ns(1_700_000_000_000_000_000) ``` ### Encoding Aliases Python accepts flexible encoding strings: | Canonical | Aliases | |-----------|---------| | `"mono8"` | `"gray"`, `"grey"`, `"l"` | | `"rgb8"` | `"rgb"` | | `"bgr8"` | `"bgr"` | | `"rgba8"` | `"rgba"` | | `"bgra8"` | `"bgra"` | | `"yuv422"` | `"yuyv"` | | `"mono32f"` | `"gray32f"`, `"float"` | | `"depth16"` | — | ## See Also - [Vision Messages](/rust/api/vision-messages) — CompressedImage, CameraInfo, Detection types - [TensorPool API](/rust/api/tensor-pool) — Advanced pool management - [Tensor Messages](/rust/api/tensor-messages) — Tensor descriptor, TensorDtype, Device - [Python Memory Types](/python/api/memory-types) — Python Image API with NumPy/PyTorch/JAX zero-copy interop --- ## PointCloud Path: /rust/api/pointcloud Description: Zero-copy shared memory point cloud type for LiDAR and 3D sensing # PointCloud HORUS provides a pool-backed RAII `PointCloud` type for LiDAR and 3D sensing workloads. Point cloud data lives in shared memory and is transported zero-copy between nodes — only a lightweight descriptor is transmitted through topics. ## Creating a PointCloud ```rust use horus::prelude::*; // XYZ point cloud: 10,000 points, 3 fields per point (x, y, z), float32 let mut cloud = PointCloud::from_xyz(\&points)? // 10_000 points; // XYZI point cloud (with intensity): 4 fields per point let mut cloud_i = PointCloud::from_xyzi(\&points)? // 50_000 points; // XYZRGB point cloud (with color): 6 fields per point let mut cloud_rgb = PointCloud::from_xyz(\&points) // 20_000 points, 6 fields?; ``` ## Writing Point Data ```rust use horus::prelude::*; let mut cloud = PointCloud::from_xyz(\&points)? // 3 points; // Copy raw bytes into the cloud let points: Vec = vec![ 1.0, 2.0, 3.0, // point 0 4.0, 5.0, 6.0, // point 1 7.0, 8.0, 9.0, // point 2 ]; cloud.copy_from_f32(&points); // Or write directly via mutable data access let data: &mut [u8] = cloud.data_mut(); // ... fill data ... ``` ## Reading Points ```rust use horus::prelude::*; let cloud = PointCloud::from_xyz(\&points)? // 10_000 points; // Extract all XYZ coordinates (F32 clouds only) if let Some(points) = cloud.extract_xyz() { for p in &points[..5] { println!("({:.2}, {:.2}, {:.2})", p[0], p[1], p[2]); } } // Access a single point as raw bytes if let Some(point_bytes) = cloud.point_at(0) { let floats = cloud.point_as_f32(0); println!("Point 0: x={}, y={}, z={}", floats[0], floats[1], floats[2]); } // Zero-copy access to the entire buffer let raw: &[u8] = cloud.data(); ``` ## Metadata and Properties ```rust use horus::prelude::*; let mut cloud = PointCloud::from_xyzi(\&points)? // 10_000 points; // Set frame and timestamp (method chaining) cloud.set_frame_id("velodyne_top") .set_timestamp_ns(1_700_000_000_000_000_000); // Read back println!("Frame: {}", cloud.frame_id()); println!("Timestamp: {} ns", cloud.timestamp_ns()); // Point layout queries println!("Points: {}", cloud.point_count()); // 10000 println!("Fields/point: {}", cloud.fields_per_point()); // 4 println!("Is XYZ: {}", cloud.is_xyz()); // false (4 fields) println!("Has intensity: {}", cloud.has_intensity()); // true println!("Has color: {}", cloud.has_color()); // false // Type info println!("Dtype: {:?}", cloud.dtype()); // F32 println!("Total bytes: {}", cloud.nbytes()); // 10000 * 4 * 4 println!("Is CPU: {}", cloud.is_cpu()); // true ``` ## Sending and Receiving via Topic Only a lightweight descriptor is transmitted through topics. The point data stays in shared memory — true zero-copy IPC. ```rust use horus::prelude::*; // Publisher let pub_topic: Topic = Topic::new("lidar.points")?; let mut cloud = PointCloud::from_xyzi(\&points)? // 64_000 points; cloud.set_frame_id("velodyne_top"); // ... fill point data from sensor driver ... pub_topic.send(cloud); ``` ```rust // Subscriber let sub_topic: Topic = Topic::new("lidar.points")?; if let Some(cloud) = sub_topic.recv() { println!("Received {} points from '{}'", cloud.point_count(), cloud.frame_id()); if let Some(xyz) = cloud.extract_xyz() { let closest = xyz.iter() .map(|p| (p[0]*p[0] + p[1]*p[1] + p[2]*p[2]).sqrt()) .fold(f32::INFINITY, f32::min); println!("Closest point: {:.2}m", closest); } } ``` ## LiDAR Processing Pipeline A complete node that receives raw LiDAR scans, filters ground points, and publishes the result: ```rust use horus::prelude::*; struct LidarFilterNode { raw_sub: Topic, filtered_pub: Topic, ground_threshold: f32, } impl Node for LidarFilterNode { fn name(&self) -> &str { "LidarFilter" } fn tick(&mut self) { if let Some(raw) = self.raw_sub.recv() { if let Some(points) = raw.extract_xyz() { // Filter out ground points (z below threshold) let non_ground: Vec = points.iter() .filter(|p| p[2] > self.ground_threshold) .flat_map(|p| p.iter().copied()) .collect(); let num_points = non_ground.len() / 3; if let Ok(mut filtered) = PointCloud::new( num_points as u32, 3, TensorDtype::F32 ) { filtered.copy_from_f32(&non_ground) .set_frame_id(raw.frame_id()) .set_timestamp_ns(raw.timestamp_ns()); self.filtered_pub.send(filtered); } } } } } ``` ## API Reference ### Constructors #### from_xyz ```rust pub fn from_xyz(points: &[[f32; 3]]) -> Result ``` Create a point cloud from XYZ coordinate arrays. **Recommended for most LiDAR and depth camera data.** **Parameters:** - `points: &[[f32; 3]]` — Array of [x, y, z] coordinates in meters. Coordinate frame: right-hand, Z-up (horus convention). **Returns:** `Result` — `Err(MemoryError::PoolExhausted)` if tensor pool is full. **Memory:** Data is copied into the SHM tensor pool. Subsequent `Topic::send()` is zero-copy. ```rust let points = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]; let cloud = PointCloud::from_xyz(&points)?; assert_eq!(cloud.point_count(), 2); assert!(cloud.is_xyz()); ``` #### from_xyzi ```rust pub fn from_xyzi(points: &[[f32; 4]]) -> Result ``` Create from XYZI arrays (with intensity). Intensity is typically 0.0-1.0 (normalized reflectance). **Parameters:** - `points: &[[f32; 4]]` — Array of [x, y, z, intensity]. Units: meters + unitless (0.0-1.0). #### from_xyzrgb ```rust pub fn from_xyzrgb(points: &[[f32; 6]]) -> Result ``` Create from XYZRGB arrays. Color values are 0.0-255.0 as floats. **Parameters:** - `points: &[[f32; 6]]` — Array of [x, y, z, r, g, b]. Units: meters + 0-255 color. | Constructor | Fields/Point | Use Case | |------------|-------------|----------| | `from_xyz()` | 3 | LiDAR, depth cameras | | `from_xyzi()` | 4 | LiDAR with reflectance | | `from_xyzrgb()` | 6 | RGB-D cameras, colored reconstructions | ### Point Access | Method | Returns | Description | |--------|---------|-------------| | `point_at(idx)` | `Option<&[u8]>` | Raw bytes of the i-th point | | `extract_xyz()` | `Option>` | All XYZ coordinates as float arrays (F32 only, validates alignment) | ### Metadata | Method | Returns | Description | |--------|---------|-------------| | `point_count()` | `u64` | Number of points in the cloud | | `fields_per_point()` | `u32` | Fields per point (3=XYZ, 4=XYZI, 6=XYZRGB) | | `is_xyz()` | `bool` | True if this is a plain XYZ cloud (3 fields) | | `has_intensity()` | `bool` | True if cloud includes an intensity field (4+ fields) | | `has_color()` | `bool` | True if cloud includes color fields (6+ fields) | ### Data Access (Zero-Copy) | Method | Returns | Description | |--------|---------|-------------| | `data()` | `&[u8]` | Immutable access to the raw point buffer | | `data_mut()` | `&mut [u8]` | Mutable access to the raw point buffer | | `copy_from(src)` | `&mut Self` | Copy bytes into the point buffer (chainable) | ### Frame and Timestamp | Method | Returns | Description | |--------|---------|-------------| | `set_frame_id(id)` | `&mut Self` | Set sensor/coordinate frame identifier (chainable) | | `set_timestamp_ns(ts)` | `&mut Self` | Set timestamp in nanoseconds (chainable) | | `frame_id()` | `&str` | Get the frame identifier | | `timestamp_ns()` | `u64` | Get timestamp in nanoseconds | ### Type Info | Method | Returns | Description | |--------|---------|-------------| | `dtype()` | `TensorDtype` | Data type of each field (e.g., F32, F64) | | `nbytes()` | `u64` | Total size of the point buffer in bytes | | `is_cpu()` | `bool` | Whether data resides on CPU (shared memory) | ## Point Types Fixed-size point structs optimized for zero-copy transport. ### PointXYZ Basic 3D point (12 bytes). ```rust use horus::prelude::*; let p = PointXYZ::new(1.0, 2.0, 3.0); println!("Distance from origin: {:.2}m", p.distance()); let q = PointXYZ::new(4.0, 6.0, 3.0); println!("Distance between: {:.2}m", p.distance_to(&q)); ``` | Field | Type | Description | |-------|------|-------------| | `x` | `f32` | X coordinate (meters) | | `y` | `f32` | Y coordinate (meters) | | `z` | `f32` | Z coordinate (meters) | | Method | Returns | Description | |--------|---------|-------------| | `new(x, y, z)` | `PointXYZ` | Create a point | | `distance()` | `f32` | Euclidean distance from the origin | | `distance_to(other)` | `f32` | Euclidean distance to another point | ### PointXYZI 3D point with intensity (16 bytes). Common for LiDAR sensors (Velodyne, Ouster, Livox). ```rust use horus::prelude::*; let p = PointXYZI::new(1.0, 2.0, 3.0, 128.0); // Convert from PointXYZ (zero intensity) let xyz = PointXYZ::new(1.0, 2.0, 3.0); let with_intensity = PointXYZI::from_xyz(xyz); // Convert back to PointXYZ (drop intensity) let xyz_only = p.xyz(); ``` | Field | Type | Description | |-------|------|-------------| | `x`, `y`, `z` | `f32` | Coordinates (meters) | | `intensity` | `f32` | Reflectance intensity (typically 0-255) | | Method | Returns | Description | |--------|---------|-------------| | `new(x, y, z, intensity)` | `PointXYZI` | Create a point with intensity | | `from_xyz(xyz)` | `PointXYZI` | Convert from PointXYZ (intensity = 0) | | `xyz()` | `PointXYZ` | Convert to PointXYZ (drop intensity) | ### PointXYZRGB 3D point with RGB color (16 bytes). Common for RGB-D cameras like Intel RealSense. ```rust use horus::prelude::*; let p = PointXYZRGB::new(1.0, 2.0, 3.0, 255, 0, 0); // Red point // Convert from PointXYZ (defaults to white) let xyz = PointXYZ::new(1.0, 2.0, 3.0); let colored = PointXYZRGB::from_xyz(xyz); // Get packed RGB as u32 (0xRRGGBBAA) let packed = p.rgb_packed(); // Convert back to PointXYZ (drop color) let xyz_only = p.xyz(); ``` | Field | Type | Description | |-------|------|-------------| | `x`, `y`, `z` | `f32` | Coordinates (meters) | | `r`, `g`, `b` | `u8` | Color components (0-255) | | `a` | `u8` | Alpha/padding (255 default) | | Method | Returns | Description | |--------|---------|-------------| | `new(x, y, z, r, g, b)` | `PointXYZRGB` | Create a colored point | | `from_xyz(xyz)` | `PointXYZRGB` | Convert from PointXYZ (white, alpha 255) | | `rgb_packed()` | `u32` | Get color as packed 0xRRGGBBAA | | `xyz()` | `PointXYZ` | Convert to PointXYZ (drop color) | ## Python API (PyPointCloud) ### Constructor and Factories ```python import horus import numpy as np # Create from scratch cloud = horus.PointCloud(num_points=10000, fields=3, dtype="float32") # Create from NumPy array (shape must be [N, fields]) arr = np.random.randn(10000, 3).astype(np.float32) cloud = horus.PointCloud.from_numpy(arr) # Create from PyTorch tensor import torch tensor = torch.randn(10000, 4) cloud = horus.PointCloud.from_torch(tensor) ``` ### Conversions ```python # Convert to NumPy (zero-copy when possible) arr = cloud.to_numpy() # shape: (N, fields) # Convert to PyTorch tensor tensor = cloud.to_torch() # shape: (N, fields) # Convert to JAX array jax_arr = cloud.to_jax() # shape: (N, fields) ``` ### Access and Properties ```python cloud = horus.PointCloud(num_points=10000, fields=4, dtype="float32") # Properties print(cloud.point_count) # 10000 print(cloud.fields_per_point) # 4 print(cloud.dtype) # "float32" print(cloud.nbytes) # 160000 # Point access coords = cloud.point_at(0) # [x, y, z, intensity] as list of floats # Metadata cloud.set_frame_id("velodyne_top") cloud.set_timestamp_ns(1700000000000000000) print(cloud.frame_id) # "velodyne_top" print(cloud.timestamp_ns) # 1700000000000000000 # Layout queries cloud.is_xyz() # False (4 fields) cloud.has_intensity() # True cloud.has_color() # False ``` ### Python LiDAR Pipeline ```python import horus import numpy as np sub = horus.Topic(horus.PointCloud, endpoint="lidar.points") pub = horus.Topic(horus.PointCloud, endpoint="lidar.filtered") while True: cloud = sub.recv() if cloud is None: continue # Convert to NumPy for processing points = cloud.to_numpy() # (N, 3) # Remove points below ground plane mask = points[:, 2] > -0.3 filtered = points[mask] # Publish filtered cloud out = horus.PointCloud.from_numpy(filtered.astype(np.float32)) out.set_frame_id(cloud.frame_id) out.set_timestamp_ns(cloud.timestamp_ns) pub.send(out) ``` ## See Also - [Perception Messages](/rust/api/perception-messages) - PointField, PointCloudHeader, DepthImage, landmarks, tracking - [Tensor Messages](/rust/api/tensor-messages) - TensorDtype, Device, auto-managed tensor pools - [TensorPool API](/rust/api/tensor-pool) - Advanced pool management - [Sensor Messages](/rust/api/sensor-messages) - LaserScan for 2D LiDAR - [Vision Messages](/rust/api/vision-messages) - Image, CameraInfo, Detection3D - [Python Memory Types](/python/api/memory-types) — Python PointCloud API with NumPy/PyTorch zero-copy interop - [Python Perception Types](/python/api/perception) — PointCloudBuffer, DetectionList, TrackedObject --- ## DepthImage Path: /rust/api/depth-image Description: Zero-copy shared memory depth image for RGB-D cameras and depth sensors # DepthImage Pool-backed depth image with zero-copy shared memory transport. Use it for obstacle detection, 3D reconstruction, or navigation costmap updates from RGB-D cameras. **Quick example** — detect obstacles closer than 1 meter: ```rust use horus::prelude::*; let depth_topic: Topic = Topic::new("camera.depth")?; if let Some(depth) = depth_topic.recv() { // Check center pixel for close obstacles let center_distance = depth.depth_at(320, 240); // meters if center_distance > 0.0 && center_distance < 1.0 { hlog!(warn, "Obstacle at {:.2}m!", center_distance); } // Find closest point in the frame if let Some(min) = depth.min_depth() { hlog!(info, "Closest point: {:.2}m", min); } } ``` DepthImage supports two formats: F32 (meters) and U16 (millimeters). All depth access methods work in meters regardless of format — U16 values are automatically converted. ## Creating a DepthImage ```rust use horus::prelude::*; // F32 depth image -- values stored in meters (default choice) let mut depth = DepthImage::meters(640, 480)?; // U16 depth image -- values stored in millimeters (common for Intel RealSense, Azure Kinect) let mut depth_mm = DepthImage::millimeters(640, 480)?; ``` ## Reading and Writing Depth ```rust use horus::prelude::*; let mut depth = DepthImage::meters(640, 480)?; // Set depth at pixel (x, y) in meters -- returns &mut Self for chaining depth.set_depth(320, 240, 1.5)?; depth.set_depth(100, 100, 2.3)?; // Get depth at pixel -- always returns meters (auto-converts U16 mm to f32 m) if let Some(d) = depth.get_depth(320, 240) { println!("Depth at center: {:.3}m", d); } // For U16 images, get raw millimeter value let depth_u16 = DepthImage::millimeters(640, 480)?; if let Some(mm) = depth_u16.get_depth_u16(320, 240) { println!("Raw depth: {}mm", mm); } // Compute min, max, mean over valid (non-zero) depth values if let Some((min, max, mean)) = depth.depth_statistics() { println!("Range: {:.2}-{:.2}m, mean: {:.2}m", min, max, mean); } ``` ## Metadata and Transport ```rust use horus::prelude::*; let mut depth = DepthImage::meters(640, 480)?; // Set metadata (method chaining) depth .set_frame_id("depth_camera") .set_timestamp_ns(1234567890); // Check format println!("{}x{}", depth.width(), depth.height()); println!("Meters: {}, Scale: {}", depth.is_meters(), depth.depth_scale()); // Zero-copy raw data access let raw: &[u8] = depth.data(); println!("{} bytes", depth.nbytes()); ``` ## RGB-D Pipeline ```rust use horus::prelude::*; struct RgbdNode { rgb_sub: Topic, depth_sub: Topic, cloud_pub: Topic, fx: f32, fy: f32, cx: f32, cy: f32, } impl Node for RgbdNode { fn name(&self) -> &str { "RgbdNode" } fn tick(&mut self) { let depth = match self.depth_sub.recv() { Some(d) => d, None => return, }; let w = depth.width(); let h = depth.height(); if let Ok(mut cloud) = PointCloud::from_xyz(&xyz_points) { // Depth-to-pointcloud using camera intrinsics for y in 0..h { for x in 0..w { if let Some(z) = depth.get_depth(x, y) { if z > 0.0 { let px = (x as f32 - self.cx) * z / self.fx; let py = (y as f32 - self.cy) * z / self.fy; // Write (px, py, z) into cloud data... } } } } cloud.set_frame_id(depth.frame_id()); self.cloud_pub.send(cloud); } } } ``` ## Rust API Reference DepthImage is an RAII type -- fields are private, accessed through methods. Mutation methods return `&mut Self` for chaining. | Method | Returns | Description | |--------|---------|-------------| | `new(width, height, dtype)` | `Result` | Create depth image (F32 or U16) from global pool | | `get_depth(x, y)` | `Option` | Depth in meters at pixel (auto-converts U16) | | `set_depth(x, y, value)` | `Result<&mut Self>` | Set depth in meters at pixel | | `get_depth_u16(x, y)` | `Option` | Raw U16 millimeters (U16 dtype only) | | `depth_statistics()` | `Option<(f32, f32, f32)>` | (min, max, mean) in meters over valid depths | | `width()` | `u32` | Image width in pixels | | `height()` | `u32` | Image height in pixels | | `is_meters()` | `bool` | True if dtype is F32 | | `is_millimeters()` | `bool` | True if dtype is U16 | | `depth_scale()` | `f32` | Depth unit scale factor | | `data()` | `&[u8]` | Zero-copy access to raw depth data | | `data_mut()` | `&mut [u8]` | Mutable access to raw depth data | | `copy_from(src)` | `&mut Self` | Copy raw bytes into the image | | `dtype()` | `TensorDtype` | Data type (F32 or U16) | | `nbytes()` | `u64` | Total size in bytes | | `is_cpu()` | `bool` | Whether data is on CPU | | `set_frame_id(id)` | `&mut Self` | Set sensor frame identifier | | `frame_id()` | `&str` | Get sensor frame identifier | | `set_timestamp_ns(ts)` | `&mut Self` | Set timestamp in nanoseconds | | `timestamp_ns()` | `u64` | Get timestamp in nanoseconds | **Depth format summary:** | Dtype | Unit | `is_meters()` | `depth_scale()` | Typical sensor | |-------|------|---------------|-----------------|----------------| | `TensorDtype::F32` | meters | `true` | `1.0` | Processed / simulated | | `TensorDtype::U16` | millimeters | `false` | `0.001` | RealSense, Kinect, stereo | ## Python API (PyDepthImage) ```python import horus import numpy as np # Create from dtype string depth = horus.DepthImage(480, 640, dtype="float32") # meters depth = horus.DepthImage(480, 640, dtype="uint16") # millimeters depth = horus.DepthImage(480, 640, dtype="mm") # alias for uint16 # Create from numpy array arr = np.zeros((480, 640), dtype=np.float32) depth = horus.DepthImage.from_numpy(arr) # Create from torch tensor import torch t = torch.zeros(480, 640, dtype=torch.float32) depth = horus.DepthImage.from_torch(t) # Read/write depth (meters) depth.set_depth(320, 240, 1.5) d = depth.get_depth(320, 240) # -> float # Statistics stats = depth.depth_statistics() # -> (min, max, mean) or None # Convert to numpy/torch/jax arr = depth.to_numpy() t = depth.to_torch() j = depth.to_jax() # Properties depth.height # 480 depth.width # 640 depth.dtype # "float32" depth.nbytes # total bytes depth.depth_scale # 1.0 for F32, 0.001 for U16 # Metadata depth.set_frame_id("depth_cam") depth.set_timestamp_ns(123456) depth.is_meters() # True depth.is_millimeters() # False ``` **Valid dtype strings:** | String | Format | |--------|--------| | `"float32"`, `"meters"` | F32, meters | | `"uint16"`, `"millimeters"`, `"mm"` | U16, millimeters | ## See Also - [Perception Messages](/rust/api/perception-messages) -- PointCloud, Landmark, TrackedObject - [Vision Messages](/rust/api/vision-messages) -- Image, CameraInfo, Detection - [TensorPool API](/rust/api/tensor-pool) -- Underlying pool management - [Python Memory Types](/python/api/memory-types) -- Python DepthImage API with NumPy/PyTorch/JAX interop --- ## Tensor Path: /rust/api/tensor Description: Zero-copy tensor descriptor with DLPack interop for ML pipelines # Tensor A lightweight tensor descriptor for zero-copy ML data sharing across nodes and processes. ```rust use horus::prelude::*; ``` ## Overview `Tensor` is a lightweight descriptor that references data in shared memory. Only the descriptor is transmitted through topics — the actual tensor data stays in-place, enabling zero-copy transport for large ML payloads. ## Methods | Method | Return Type | Description | |--------|-------------|-------------| | `shape()` | `&[u64]` | Tensor dimensions (e.g., `[1080, 1920, 3]`) | | `strides()` | `&[u64]` | Byte strides per dimension | | `numel()` | `u64` | Total number of elements | | `nbytes()` | `u64` | Total size in bytes (`numel * dtype.element_size()`) | | `dtype()` | `TensorDtype` | Element data type | | `device()` | `Device` | Device location (CPU or CUDA) | | `is_cpu()` | `bool` | True if data resides on CPU / shared memory | | `is_cuda()` | `bool` | True if device descriptor is set to CUDA | | `is_contiguous()` | `bool` | True if memory layout is C-contiguous | | `view(new_shape)` | `Option` | Reshape without copying (fails if not contiguous or element count changes) | | `slice_first_dim(start, end)` | `Option` | Slice along the first dimension, adjusting strides | ### Reshape and Slice ```rust let topic: Topic = Topic::new("model.input")?; if let Some(tensor) = topic.recv() { // Reshape a flat 1D tensor into a batch of images if let Some(reshaped) = tensor.view(&[4, 3, 224, 224]) { println!("Batch shape: {:?}", reshaped.shape()); // [4, 3, 224, 224] } // Take the first 2 items from a batch if let Some(sliced) = tensor.slice_first_dim(0, 2) { println!("Sliced shape: {:?}", sliced.shape()); // [2, 3, 224, 224] } } ``` ## TensorDtype Supported element types with sizes and common use cases: | Dtype | Size | Use Case | |-------|------|----------| | `F32` | 4 bytes | ML training and inference | | `F64` | 8 bytes | High-precision computation | | `F16` | 2 bytes | Memory-efficient inference | | `BF16` | 2 bytes | Training on modern GPUs | | `I8` | 1 byte | Quantized inference | | `I16` | 2 bytes | Audio, sensor data | | `I32` | 4 bytes | General integer | | `I64` | 8 bytes | Large signed values | | `U8` | 1 byte | Images | | `U16` | 2 bytes | Depth sensors (mm) | | `U32` | 4 bytes | Large indices | | `U64` | 8 bytes | Counters, timestamps | | `Bool` | 1 byte | Masks | ### TensorDtype Methods ```rust let dtype = TensorDtype::F32; // Size in bytes assert_eq!(dtype.element_size(), 4); // Display (lowercase string representation) println!("{}", dtype); // "float32" // Parse from string — accepts common aliases let parsed = TensorDtype::parse("float32").unwrap(); // F32 let parsed = TensorDtype::parse("f16").unwrap(); // F16 let parsed = TensorDtype::parse("uint8").unwrap(); // U8 let parsed = TensorDtype::parse("bool").unwrap(); // Bool ``` ## Device Fixed-size device descriptor supporting CPU and CUDA device tags. `Device` is metadata only — `Device::cuda(N)` tags a tensor with a device target but does not allocate GPU memory (GPU tensor pools are not yet implemented). ```rust // Constructors let cpu = Device::cpu(); let gpu0 = Device::cuda(0); // Descriptor only — no GPU allocation // Check device type assert!(cpu.is_cpu()); assert!(gpu0.is_cuda()); // Display println!("{}", gpu0); // "cuda:0" // Parse from string let dev = Device::parse("cpu").unwrap(); let dev = Device::parse("cuda:0").unwrap(); ``` ## ML Pipeline Example A camera node captures frames using `Image`, while a preprocessing node converts them into batched `Tensor` data for model inference: ```rust use horus::prelude::*; // Producer: camera capture node — uses Image, not raw Tensor node! { CameraNode { pub { frames: Image -> "camera.rgb" } data { frame_count: u64 = 0 } tick { let image = Image::new(640, 480, ImageEncoding::Rgb8); // ... fill pixel data from camera driver ... self.frames.send(&image); self.frame_count += 1; } } } // Preprocessor: converts Image frames into batched Tensor for ML node! { PreprocessNode { sub { frames: Image -> "camera.rgb" } pub { batch: Tensor -> "model.input" } data { buffer: Vec = Vec::new() } tick { if let Some(img) = self.frames.recv() { self.buffer.push(img); if self.buffer.len() >= 4 { // Build a [4, 3, 224, 224] F32 batch tensor for the model let tensor = Tensor::from_shape( &[4, 3, 224, 224], TensorDtype::F32, Device::cpu(), ); // ... resize, normalize, and copy frames into tensor ... self.batch.send(&tensor); self.buffer.clear(); } } } } } // Consumer: inference node — works with raw Tensor input/output node! { InferenceNode { sub { input: Tensor -> "model.input" } pub { detections: GenericMessage -> "model.detections" } tick { if let Some(tensor) = self.input.recv() { hlog!(debug, "Input: {:?}, {} bytes", tensor.shape(), tensor.nbytes()); // Run inference on the batch tensor, publish results ... } } } } ``` ## Python Usage In Python, use `Image`, `PointCloud`, or `DepthImage` for zero-copy tensor data — they wrap the pool-backed tensor system automatically and provide `.to_numpy()`, `.to_torch()`, `.to_jax()` conversions: ```python import horus import numpy as np # Image → NumPy (zero-copy) img = horus.Image(480, 640, "rgb8") arr = img.to_numpy() # shape: (480, 640, 3), dtype: uint8 # PointCloud → PyTorch (zero-copy via DLPack) cloud = horus.PointCloud.from_numpy(np.random.randn(1000, 3).astype(np.float32)) tensor = cloud.to_torch() # DepthImage → JAX depth = horus.DepthImage(480, 640, "float32") jax_arr = depth.to_jax() ``` See [Python Memory Types](/python/api/memory-types) for the full API. --- ## TensorDtype Enumerates all supported tensor element types. Matches common ML framework dtypes for seamless interop with PyTorch, NumPy, JAX, and DLPack. | Variant | Value | Size | NumPy | Use Case | |---------|-------|------|-------|----------| | `F32` | 0 | 4 bytes | ` = Topic::new("external.detections")?; topic.send(msg); ``` ```python # Python side: receive and parse topic = Topic(GenericMessage, "external.detections") msg = topic.recv() data = msg.to_dict() # {"class": "box", "confidence": 0.95, ...} ``` ## When to Use Use `GenericMessage` when standard types (`CmdVel`, `Imu`, `LaserScan`, etc.) don't cover your data: - Sending structured data from Rust to Python (or vice versa) - Prototyping a custom message before defining a dedicated type - Bridging external systems that produce arbitrary payloads **Prefer typed messages whenever possible.** Typed POD messages achieve ~200ns latency via zero-copy shared memory. `GenericMessage` requires MessagePack serialization, which adds overhead (~4us). Use it only when you need the flexibility. ## Constructors ### From Raw Bytes ```rust let data: Vec = vec![0x01, 0x02, 0x03]; let msg = GenericMessage::new(data)?; ``` Maximum payload is **4,096 bytes** (4KB). Returns an error if exceeded. ### With Metadata ```rust let data: Vec = sensor_bytes.to_vec(); let msg = GenericMessage::with_metadata(data, "lidar_raw".to_string())?; ``` Metadata is an optional string label (max **255 bytes**). Useful for tagging the content type so the receiver knows how to interpret the payload. ### From a Serializable Value ```rust use std::collections::HashMap; let mut config = HashMap::new(); config.insert("gain", 1.5_f64); config.insert("offset", 0.3); let msg = GenericMessage::from_value(&config)?; ``` Serializes any `T: Serialize` via MessagePack into the payload bytes. ## Methods | Method | Return Type | Description | |--------|-------------|-------------| | `data()` | `Vec` | Get the raw payload bytes | | `metadata()` | `Option` | Get the metadata string, if set | | `to_value::()` | `Result` | Deserialize payload from MessagePack into `T` | ## Examples ### Rust to Rust ```rust use horus::prelude::*; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] struct CalibrationData { offsets: Vec, scale: f64, label: String, } // Sender let cal = CalibrationData { offsets: vec![0.1, -0.05, 0.02], scale: 1.001, label: "imu_0".into(), }; let topic: Topic = Topic::new("calibration")?; topic.send(GenericMessage::from_value(&cal)?); // Receiver if let Some(msg) = topic.recv() { let cal: CalibrationData = msg.to_value()?; println!("Scale: {}, offsets: {:?}", cal.scale, cal.offsets); } ``` ### Rust to Python A Rust node publishes config data that a Python node consumes: ```rust // Rust sender use std::collections::HashMap; let mut params = HashMap::new(); params.insert("kp", 1.2_f64); params.insert("ki", 0.01); params.insert("kd", 0.5); let topic: Topic = Topic::new("controller.params")?; let msg = GenericMessage::with_metadata( GenericMessage::from_value(¶ms)?.data(), "pid_gains".to_string(), )?; topic.send(msg); ``` ```python # Python receiver import horus topic = horus.Topic(horus.GenericMessage, endpoint="controller.params") msg = topic.recv() if msg is not None: params = msg.to_dict() # Deserializes MessagePack print(f"PID gains: kp={params['kp']}, ki={params['ki']}, kd={params['kd']}") if msg.metadata == "pid_gains": controller.update_gains(**params) ``` ### With Metadata Tagging ```rust // Tag messages so the receiver can dispatch by type let msg = GenericMessage::with_metadata( GenericMessage::from_value(&sensor_reading)?.data(), "temperature".to_string(), )?; topic.send(msg); // Receiver dispatches based on metadata if let Some(msg) = topic.recv() { match msg.metadata().as_deref() { Some("temperature") => { let temp: f64 = msg.to_value()?; hlog!(info, "Temperature: {:.1} C", temp); } Some("humidity") => { let hum: f64 = msg.to_value()?; hlog!(info, "Humidity: {:.1}%", hum); } _ => { hlog!(warn, "Unknown message type"); } } } ``` ## Performance `GenericMessage` uses MessagePack serialization, which adds overhead compared to typed POD messages: | Payload Size | GenericMessage Latency | Typed Message Latency | |-------------|----------------------|----------------------| | Small (up to 256 bytes) | ~4.0 us | ~200 ns | | Large (up to 4 KB) | ~4.4 us | ~200 ns | The 20x difference comes from serialization/deserialization. For high-frequency data (IMU at 1 kHz, motor commands at 500 Hz), always use typed messages. `GenericMessage` is appropriate for lower-frequency data like configuration updates, calibration parameters, or diagnostic payloads. ## Limits | Constraint | Value | |-----------|-------| | Maximum payload | 4,096 bytes (4 KB) | | Maximum metadata | 255 bytes | | Serialization format | MessagePack | The 4KB limit keeps `GenericMessage` as a fixed-size type suitable for shared memory transport. For larger payloads, use `Topic` with [TensorPool](/rust/api/tensor-pool) instead. ## Pitfalls **No compile-time type safety.** Unlike `Topic`, `Topic` can carry any data. A publisher sending `{"velocity": 1.0}` and a subscriber expecting `{"speed": 1.0}` will silently fail — the key name mismatch is only caught at runtime. **Debugging is harder.** GenericMessage shows as raw bytes in the monitor. Typed messages show structured fields. Prefer typed messages for anything you'll need to debug. **Not for high-frequency data.** The 20x latency overhead (4μs vs 200ns) matters at 1kHz+. Use typed messages (`CmdVel`, `Imu`, etc.) for control loops and sensor streams. **When to use GenericMessage:** - Python↔Python communication with arbitrary dicts - Configuration updates at low frequency - Prototyping before defining a typed message - Cross-language payloads where defining a shared struct isn't worth it ## See Also - [Standard Messages](/rust/api/messages) -- All built-in message types - [TensorPool API](/rust/api/tensor-pool) -- For payloads larger than 4KB - [message! Macro](/rust/api/macros#message) -- Define custom typed messages --- ## Input Messages Path: /rust/api/input-messages Description: Keyboard and joystick/gamepad input messages for teleoperation and HID control # Input Messages HORUS provides input message types for teleoperation and human interface device (HID) control. Both types are fixed-size (72 bytes), optimized for zero-copy shared memory transport. ## KeyboardInput Keyboard key press/release events with modifier tracking. ```rust use horus::prelude::*; // Create a key press event let key = KeyboardInput::new( "w".to_string(), // key name 87, // raw key code vec!["Shift".into()], // active modifiers true // pressed ); // Check key state println!("Key: {}", key.key_name()); println!("Pressed: {}", key.is_pressed()); // Check individual modifiers if key.is_ctrl() { println!("Ctrl is held"); } if key.is_shift() { println!("Shift is held"); } // Check any modifier by name if key.has_modifier("Alt") { println!("Alt is held"); } // Get all active modifiers as a list let mods = key.modifiers(); // Vec println!("Active modifiers: {:?}", mods); ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `key_name` | `[u8; 32]` | — | Key name buffer (null-terminated, max 31 chars) | | `code` | `u32` | — | Raw key code | | `modifier_flags` | `u32` | — | Bit flags for active modifiers | | `pressed` | `u8` | — | 1 = press, 0 = release | | `timestamp_ms` | `u64` | ms | Unix timestamp in milliseconds | **Modifier Flags:** | Constant | Value | Description | |----------|-------|-------------| | `MODIFIER_CTRL` | `1 << 0` | Control key | | `MODIFIER_ALT` | `1 << 1` | Alt key | | `MODIFIER_SHIFT` | `1 << 2` | Shift key | | `MODIFIER_SUPER` | `1 << 3` | Super/Windows/Cmd key | | `MODIFIER_HYPER` | `1 << 4` | Hyper key | | `MODIFIER_META` | `1 << 5` | Meta key | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(key, code, modifiers, pressed)` | `KeyboardInput` | Create from key name, code, modifier names, pressed state | | `key_name()` | `String` | Get key name from buffer | | `is_pressed()` | `bool` | Check if key is pressed | | `modifiers()` | `Vec` | Get all active modifiers as string names | | `has_modifier(name)` | `bool` | Check modifier by name ("Ctrl", "Alt", "Shift", "Super", "Hyper", "Meta") | | `is_ctrl()` | `bool` | Ctrl held | | `is_shift()` | `bool` | Shift held | | `is_alt()` | `bool` | Alt held | ## JoystickInput Gamepad/joystick events for buttons, axes, hats, and connection state. ```rust use horus::prelude::*; // Button press let btn = JoystickInput::new_button(0, 1, "A".to_string(), true); println!("Button {} pressed: {}", btn.element_name(), btn.is_pressed()); // Axis movement (value: -1.0 to 1.0) let axis = JoystickInput::new_axis(0, 0, "LeftStickX".to_string(), 0.75); println!("Axis {}: {:.2}", axis.element_name(), axis.value); // Hat/D-pad let hat = JoystickInput::new_hat(0, 0, "DPad".to_string(), 1.0); // Controller connection event let conn = JoystickInput::new_connection(0, true); // joystick 0 connected if conn.is_connection_event() { println!("Controller {}: connected={}", conn.joystick_id, conn.is_connected()); } // Check event type if axis.is_axis() { println!("Axis event: {}", axis.value); } else if btn.is_button() { println!("Button event: pressed={}", btn.is_pressed()); } ``` **Fields:** | Field | Type | Unit | Description | |-------|------|------|-------------| | `joystick_id` | `u32` | — | Controller ID (0, 1, 2, ...) | | `event_type` | `[u8; 16]` | — | Event type: `"button"`, `"axis"`, `"hat"`, `"connection"` | | `element_id` | `u32` | — | Button/axis/hat number | | `element_name` | `[u8; 32]` | — | Element name (null-terminated, max 31 chars) | | `value` | `f32` | — | Button: 0.0/1.0, Axis: -1.0 to 1.0, Hat: directional | | `pressed` | `u8` | — | For buttons: 1 = pressed, 0 = released | | `timestamp_ms` | `u64` | ms | Unix timestamp in milliseconds | > **ROS2 equivalent:** `sensor_msgs/msg/Joy` **Constructors:** | Constructor | Description | |-------------|-------------| | `new_button(joystick_id, button_id, name, pressed)` | Button press/release event | | `new_axis(joystick_id, axis_id, name, value)` | Axis movement event | | `new_hat(joystick_id, hat_id, name, value)` | Hat/D-pad event | | `new_connection(joystick_id, connected)` | Controller connect/disconnect | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `event_type()` | `String` | Get event type string | | `element_name()` | `String` | Get element name string | | `is_button()` | `bool` | Is a button event | | `is_axis()` | `bool` | Is an axis event | | `is_hat()` | `bool` | Is a hat/D-pad event | | `is_connection_event()` | `bool` | Is a connection event | | `is_pressed()` | `bool` | Button is pressed | | `is_connected()` | `bool` | Controller is connected (connection events) | ## Teleoperation Example ```rust use horus::prelude::*; struct TeleoperationNode { keyboard_sub: Topic, joystick_sub: Topic, cmd_vel_pub: Topic, linear_speed: f64, angular_speed: f64, } impl Node for TeleoperationNode { fn name(&self) -> &str { "Teleop" } fn tick(&mut self) { let mut linear = 0.0; let mut angular = 0.0; // Process keyboard (WASD) if let Some(key) = self.keyboard_sub.recv() { if key.is_pressed() { match key.key_name().as_str() { "w" => linear = self.linear_speed, "s" => linear = -self.linear_speed, "a" => angular = self.angular_speed, "d" => angular = -self.angular_speed, _ => {} } // Shift for boost if key.is_shift() { linear *= 2.0; angular *= 2.0; } } } // Process joystick (overrides keyboard if present) if let Some(joy) = self.joystick_sub.recv() { if joy.is_axis() { match joy.element_name().as_str() { "LeftStickY" => linear = joy.value as f64 * self.linear_speed, "RightStickX" => angular = joy.value as f64 * self.angular_speed, _ => {} } } } self.cmd_vel_pub.send(Twist::new_2d(linear, angular)); } } ``` ## See Also - [Sensor Messages](/rust/api/sensor-messages) - Lidar, IMU, GPS sensor data - [Control Messages](/rust/api/control-messages) - Motor and servo commands --- ## Clock & Time Messages Path: /rust/api/clock-messages Description: Simulation clock, replay time, and time synchronization messages # Clock & Time Messages HORUS provides clock and time synchronization messages for simulation, replay, and multi-sensor time alignment. Both types are fixed-size, optimized for zero-copy shared memory transport. ## Clock Simulation and replay time source. Allows nodes to operate in simulated time instead of wall-clock time. ```rust use horus::prelude::*; // Real-time wall clock let wall = Clock::wall_clock(); // Simulation time at 2x speed let sim = Clock::sim_time(5_000_000_000, 2.0); // 5 seconds sim time, 2x speed println!("Sim time: {} ns", sim.clock_ns); println!("Speed: {}x", sim.sim_speed); // Replay time (playing back recorded data) let replay = Clock::replay_time(10_000_000_000, 0.5); // 10s replay time, half speed // Pause/resume let paused = sim.set_paused(true); println!("Paused: {}", paused.is_paused()); // Measure elapsed time between clock messages let earlier = Clock::sim_time(1_000_000_000, 1.0); let later = Clock::sim_time(2_000_000_000, 1.0); let elapsed = later.elapsed_since(&earlier); println!("Elapsed: {} ns", elapsed); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `clock_ns` | `u64` | Current simulation/replay time in nanoseconds | | `realtime_ns` | `u64` | Wall-clock time for comparison | | `sim_speed` | `f64` | Playback speed (1.0 = real-time, 2.0 = 2x, 0.5 = half) | | `paused` | `u8` | 0 = running, 1 = paused | | `source` | `u8` | Time source (see constants) | | `timestamp_ns` | `u64` | When this message was published | **Source Constants:** | Constant | Value | Description | |----------|-------|-------------| | `SOURCE_WALL` | 0 | Wall-clock (real time) | | `SOURCE_SIM` | 1 | Simulation time | | `SOURCE_REPLAY` | 2 | Replay/playback time | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `wall_clock()` | `Clock` | Create real-time clock | | `sim_time(sim_ns, speed)` | `Clock` | Create simulation time clock | | `replay_time(replay_ns, speed)` | `Clock` | Create replay time clock | | `elapsed_since(&earlier)` | `u64` | Nanoseconds between two clock messages | | `is_paused()` | `bool` | Check if clock is paused | | `set_paused(paused)` | `Clock` | Return new clock with paused state (builder) | ## TimeReference External time source for synchronization between sensors or between local clock and GPS/NTP/PTP time. ```rust use horus::prelude::*; // GPS time reference with offset let time_ref = TimeReference::new( 1_709_000_000_000_000_000, // GPS time in nanoseconds "gps", // source name -500_000, // offset: local is 500µs behind GPS ); println!("Source: {}", time_ref.source_name()); // Correct a local timestamp using the reference offset let local_ts = 1_709_000_001_000_000_000; let corrected = time_ref.correct_timestamp(local_ts); println!("Corrected timestamp: {} ns", corrected); ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `time_ref_ns` | `u64` | External reference time in nanoseconds | | `source` | `[u8; 32]` | Source identifier (null-terminated): `"gps"`, `"ntp"`, `"ptp"` | | `offset_ns` | `i64` | Signed offset: `local_time - reference_time` (nanoseconds) | | `timestamp_ns` | `u64` | When this message was published | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `new(time_ref_ns, source, offset_ns)` | `TimeReference` | Create time reference | | `source_name()` | `&str` | Get source identifier as string | | `correct_timestamp(local_ns)` | `u64` | Apply offset to correct a local timestamp | ## Simulation Time Node Example ```rust use horus::prelude::*; struct SimAwareNode { clock_sub: Topic, current_time_ns: u64, is_paused: bool, } impl Node for SimAwareNode { fn name(&self) -> &str { "SimAwareNode" } fn tick(&mut self) { // Read the latest clock message if let Some(clock) = self.clock_sub.recv() { self.is_paused = clock.is_paused(); self.current_time_ns = clock.clock_ns; // Skip processing when paused if self.is_paused { return; } // Use sim time for physics, animation, etc. let sim_seconds = self.current_time_ns as f64 / 1e9; println!("Sim time: {:.3}s (speed: {}x)", sim_seconds, clock.sim_speed); } } } ``` ## Time Synchronization Example ```rust use horus::prelude::*; struct TimeSyncNode { gps_time_sub: Topic, ntp_time_sub: Topic, best_offset_ns: i64, } impl Node for TimeSyncNode { fn name(&self) -> &str { "TimeSync" } fn tick(&mut self) { // Prefer GPS time when available if let Some(gps) = self.gps_time_sub.recv() { self.best_offset_ns = gps.offset_ns; } else if let Some(ntp) = self.ntp_time_sub.recv() { self.best_offset_ns = ntp.offset_ns; } } } ``` ## See Also - [Sensor Messages](/rust/api/sensor-messages) - NavSatFix for GPS data - [Diagnostics Messages](/rust/api/diagnostics-messages) - Heartbeat for system health --- ## Clock & Time API Path: /rust/api/clock-api Description: Unified time system for wall clock, deterministic simulation, and replay modes # Clock & Time API HORUS provides a unified time system that transparently switches between real time, simulation time, and replay time. Your node code is identical in all three modes — the scheduler selects the clock backend. ## Clock Backends | Backend | Activated By | Behavior | |---------|-------------|----------| | **WallClock** | Default (normal mode) | Passthrough to `Instant::now()`. Zero overhead. | | **SimClock** | `.deterministic(true)` | Virtual time, advances by exact `dt` per tick. | | **ReplayClock** | `.replay_from(path)` | Steps through recorded timestamps. | ```rust use horus::prelude::*; // Normal mode — uses WallClock (default) let mut scheduler = Scheduler::new(); // Deterministic mode — uses SimClock let mut scheduler = Scheduler::new().deterministic(true); // Replay mode — uses ReplayClock let mut scheduler = Scheduler::new().replay_from("recording.hbag"); ``` ## Accessing Time in Nodes Inside your `tick()` function, use the `horus::` time functions. These read from whichever clock backend is active: ```rust use horus::prelude::*; struct MyNode; impl Node for MyNode { fn name(&self) -> &str { "my_node" } fn tick(&mut self) { // Current time (ClockInstant) let now = horus::now(); // Time since last tick (Duration) let dt = horus::dt(); // Total elapsed since scheduler start (Duration) let elapsed = horus::elapsed(); // Use dt for physics integration let velocity = self.acceleration * dt.as_secs_f64(); } } ``` ## Deterministic Mode When `.deterministic(true)` is set, the scheduler uses `SimClock`: - Time advances by **exactly** `1/tick_rate` per tick - Two runs with the same inputs produce **identical** clock values - No dependency on CPU speed or system load - Essential for: reproducible tests, sim-to-real parity, CI determinism ```rust use horus::prelude::*; let mut scheduler = Scheduler::new() .tick_rate(100.hz()) // 100 Hz → dt = 10ms .deterministic(true); // SimClock: virtual time scheduler.add(PhysicsNode::new()).build(); // After 100 ticks: elapsed = exactly 1.000000000 second // Regardless of how long the ticks actually took on CPU scheduler.run(); ``` ## Replay Mode When using `.replay_from(path)` or `.add_replay(path, priority)`: - Time steps through **recorded timestamps** from the `.hbag` file - Nodes experience the same timing as the original recording - Can mix live nodes with replay nodes in the same scheduler ```rust use horus::prelude::*; // Full replay — all nodes from recording let mut scheduler = Scheduler::new() .replay_from("session.hbag"); // Mixed — 1 replay node + 1 live node let mut scheduler = Scheduler::new(); scheduler.add_replay("sensor_recording.hbag", 0); // replay sensor data scheduler.add(PlannerNode::new()).build(); // live planner scheduler.run(); ``` ## Measuring Elapsed Time `horus::now()` returns a time instant. Subtract two instants to get elapsed duration: ```rust let t1 = horus::now(); // ... do work ... let t2 = horus::now(); let work_time: Duration = t2.elapsed_since(t1); // Or equivalently: let work_time: Duration = t2 - t1; ``` ## Clock Trait (Internal) The `Clock` trait is internal (`#[doc(hidden)]`) — users should use `horus::now()`, `horus::dt()`, `horus::elapsed()` instead. It's documented here for framework contributors: | Method | Description | |--------|-------------| | `now()` | Current `ClockInstant` | | `advance(dt)` | Advance by one tick's duration (no-op for WallClock) | | `reset()` | Reset to initial state (used by replay `seek(0)`) | | `elapsed()` | Total time since clock construction | ## See Also - [Deterministic Mode](/advanced/deterministic-mode) — Reproducible simulations - [Record & Replay](/advanced/record-replay) — Recording and playback - [Real-Time Concepts](/concepts/real-time) — Budgets, deadlines, and scheduling - [Clock Messages](/rust/api/clock-messages) — Clock message type for pub/sub time sync --- ## Cargo Feature Flags Path: /rust/api/feature-flags Description: Complete reference for HORUS Cargo feature flags across all crates # Cargo Feature Flags HORUS uses Cargo feature flags to keep the default binary small while allowing opt-in to heavier capabilities. This page lists every feature flag across the workspace. ## `horus` (Main Crate) The crate you add to `Cargo.toml`. Re-exports `horus_core` and `horus_library`. ```toml [dependencies] horus = "0.1" ``` | Feature | Default | What It Enables | |---------|---------|-----------------| | `macros` | **Yes** | Procedural macros: `message!`, `service!`, `action!`, `node!`, `hlog!` | | `telemetry` | **Yes** | Live monitoring export (HTTP, UDP, file) | | `blackbox` | **Yes** | Post-mortem flight recorder via `.blackbox(size_mb)` | ### Disabling defaults To build a minimal binary without macros, telemetry, or blackbox: ```toml [dependencies] horus = { version = "0.1", default-features = false } ``` To selectively re-enable: ```toml [dependencies] horus = { version = "0.1", default-features = false, features = ["macros"] } ``` --- ## `horus_library` (Message & Transform Library) These flags control optional blocking/async APIs in the transform frame system. | Feature | Default | What It Enables | |---------|---------|-----------------| | `wait` | No | Blocking `wait_for_transform()` using condvar — zero overhead when disabled | | `async-wait` | No | Async `wait_for_transform_async()` using `tokio::sync::Notify` — pulls in `tokio` | ### Usage ```toml [dependencies] horus_library = { version = "0.1", features = ["wait"] } ``` ```rust // Only available with "wait" feature let tf = tf_tree.wait_for_transform("lidar", "world", Duration::from_secs(1))?; ``` ```toml [dependencies] horus_library = { version = "0.1", features = ["async-wait"] } ``` ```rust // Only available with "async-wait" feature let tf = tf_tree.wait_for_transform_async("lidar", "world", Duration::from_secs(1)).await?; ``` --- ## `horus_core` (Runtime) These mirror the `horus` crate flags and are typically set transitively. | Feature | Default | What It Enables | |---------|---------|-----------------| | `macros` | **Yes** | Includes `horus_macros` proc-macro crate | | `telemetry` | **Yes** | Telemetry export subsystem | | `blackbox` | **Yes** | Flight recorder subsystem | | `test-utils` | No | Test utilities like `MockTopic` for downstream crate testing | ### Using test-utils Enable `test-utils` in dev-dependencies for testing: ```toml [dev-dependencies] horus_core = { version = "0.1", features = ["test-utils"] } ``` --- ## `horus_py` (Python Bindings) | Feature | Default | What It Enables | |---------|---------|-----------------| | `extension-module` | **Yes** | Required for building the `.so` Python extension | **Important**: Disable when running Rust unit tests (test binary cannot link against libpython): ```bash cargo test -p horus_py --no-default-features ``` --- ## `horus_manager` (CLI) | Feature | Default | What It Enables | |---------|---------|-----------------| | `schema` | No | JSON Schema generation for manifest types (`schemars`) | --- ## Summary Table | Crate | Feature | Default | Dependency | |-------|---------|---------|------------| | `horus` | `macros` | Yes | `horus_macros` | | `horus` | `telemetry` | Yes | — (marker) | | `horus` | `blackbox` | Yes | — (marker) | | `horus_library` | `wait` | No | — (condvar) | | `horus_library` | `async-wait` | No | `tokio` | | `horus_core` | `test-utils` | No | — (marker) | | `horus_py` | `extension-module` | Yes | `pyo3` | | `horus_manager` | `schema` | No | `schemars` | ## See Also - [Configuration Reference](/package-management/configuration) — `horus.toml` project config - [Prelude Contents](/rust/api#prelude-contents) — What `use horus::prelude::*` includes - [Performance](/performance/performance) — Optimization guide --- ## Rust Examples Path: /rust/examples Description: Working examples demonstrating HORUS patterns in Rust # Rust Examples Learn HORUS through working examples. ## [Basic Examples](/rust/examples/basic-examples) Fundamental patterns for beginners: - Publisher-Subscriber communication - Multi-node systems - Error handling patterns ## Advanced Examples See [Advanced Examples](/rust/examples/advanced-examples) for complex patterns including: - Multi-process systems - Cross-language (Rust + Python) systems --- ## Rust Documentation Path: /rust Description: HORUS Rust API, library, and examples # Rust Documentation Complete Rust documentation for the HORUS robotics framework. ## Sections ### [API Reference](/rust/api) Core Rust API documentation including Node, Topic, Scheduler, and message types. ### [Examples](/rust/examples) Working examples demonstrating HORUS patterns. - [Basic Examples](/rust/examples/basic-examples) - Publisher-subscriber, multi-node systems - [Advanced Examples](/rust/examples/advanced-examples) - Complex patterns and integrations ======================================== # SECTION: Rust Guide ======================================== --- ## Rust Guide Path: /rust-guide Description: Build robotics applications in Rust with HORUS — from your first node to production deployment # Rust Guide HORUS is built in Rust. This guide covers everything you need to write robotics applications in Rust. ## Quick Start ```rust use horus::prelude::*; struct MyNode; impl Node for MyNode { fn name(&self) -> &str { "my_node" } fn tick(&mut self) { // Your robot logic here — runs every cycle } } fn main() -> Result<()> { let mut scheduler = Scheduler::new().tick_rate(100_u64.hz()); scheduler.add(MyNode).order(0).build()?; scheduler.run() } ``` ## Essential Import ```rust use horus::prelude::*; ``` This single import gives you everything: `Node`, `Topic`, `Scheduler`, `Image`, `CmdVel`, `LaserScan`, all message types, `DurationExt` (`.hz()`, `.ms()`), and macros (`message!`, `service!`, `action!`). ## Crate Overview | Crate | What it contains | When you use it | |-------|-----------------|-----------------| | `horus` | Prelude + re-exports | Always — this is your only dependency | | `horus_core` | Scheduler, nodes, topics, services | Internal — accessed through `horus::prelude` | | `horus_library` | Messages, TransformFrame | Internal — accessed through `horus::prelude` | ## Guide Contents 1. **[Topics & Communication](/rust-guide/topics)** — Pub/sub, zero-copy, custom messages, communication patterns 2. **[Nodes & Lifecycle](/concepts/core-concepts-nodes)** — The Node trait, init/tick/shutdown, builder API 3. **[Scheduler](/concepts/core-concepts-scheduler)** — Tick loop, execution classes, RT configuration 4. **[node! Macro](/concepts/node-macro)** — Declarative node definition 5. **[Examples](/rust/examples)** — Working code you can copy and modify --- ## Topics & Communication Path: /rust-guide/topics Description: Publish and subscribe to typed messages between nodes using zero-copy shared memory topics # Topics & Communication in Rust > **This is a usage guide.** For the complete API reference with per-method signatures, parameters, return types, and error conditions, see the [Topic API Reference](/rust/api/topic). `Topic` is the primary way nodes communicate in HORUS. Topics are typed, shared-memory channels that connect publishers and subscribers automatically by name. ## Creating a Topic ```rust use horus::prelude::*; // Type is inferred from usage let cmd: Topic = Topic::new("cmd_vel")?; // Turbofish syntax let scan = Topic::::new("scan")?; // Custom capacity and slot size let lidar = Topic::::with_capacity("scan", 8, 4096)?; ``` Two topics with the same name and type connect automatically — one publishes, the other subscribes. ## Sending Messages ```rust use horus::prelude::*; let topic = Topic::::new("cmd_vel")?; // Fire-and-forget (overwrites oldest if buffer is full) topic.send(CmdVel { linear: 0.5, angular: 0.1 }); // Try without blocking — returns the message back on failure match topic.try_send(CmdVel { linear: 0.5, angular: 0.1 }) { Ok(()) => { /* sent */ } Err(msg) => { /* buffer full, msg returned */ } } // Block up to 10ms waiting for space match topic.send_blocking(CmdVel { linear: 1.0, angular: 0.0 }, 10_u64.ms()) { Ok(()) => { /* sent */ } Err(SendBlockingError::Timeout) => { /* timed out */ } } ``` | Method | Behavior | |--------|----------| | `send(msg)` | Always succeeds. Drops oldest message if full | | `try_send(msg)` | Returns `Err(msg)` if buffer is full | | `send_blocking(msg, timeout)` | Blocks until space is available or timeout elapses | ## Receiving Messages ```rust use horus::prelude::*; let topic = Topic::::new("imu")?; // Get next unread message (FIFO order) if let Some(msg) = topic.recv() { println!("Accel: {}", msg.linear_accel.x); } // Drain all pending messages while let Some(msg) = topic.recv() { process(msg); } // Skip to the latest value (requires T: Copy) if let Some(latest) = topic.read_latest() { println!("Latest orientation: {:?}", latest.orientation); } ``` Use `recv()` when you need every message in order. Use `read_latest()` for state-like data (sensor readings, poses) where only the newest value matters. ## Zero-Copy for Large Data For `Image`, `PointCloud`, `DepthImage`, and `Tensor`, HORUS uses pool-backed allocation. Only a descriptor goes through the ring buffer — the payload stays in shared memory. ```rust use horus::prelude::*; let camera = Topic::::new("camera/rgb")?; // Send — moves the Image into the pool slot camera.send(image); // Receive — zero-copy access to the image data if let Some(img) = camera.recv() { println!("{}x{} image received", img.width(), img.height()); } ``` The same API works for all pool-backed types: ```rust let cloud_topic = Topic::::new("lidar/points")?; let depth_topic = Topic::::new("camera/depth")?; let tensor_topic = Topic::::new("model/output")?; ``` ## Custom Message Types Define your own messages with the `message!` macro: ```rust use horus::prelude::*; message! { /// Motor feedback at 1kHz pub struct MotorFeedback { pub motor_id: u32, pub velocity: f32, pub current_amps: f32, pub temperature_c: f32, } } let feedback = Topic::::new("motor.feedback")?; feedback.send(MotorFeedback { motor_id: 1, velocity: 3.14, current_amps: 0.5, temperature_c: 45.0, }); ``` The macro generates serialization traits automatically. Fixed-size types get the zero-copy fast path (~50ns); variable-size types (containing `String`, `Vec`) use bincode (~167ns). ## Using Topics in Nodes Topics are typically stored as fields on your node struct: ```rust use horus::prelude::*; struct ObstacleAvoider { scan_in: Topic, cmd_out: Topic, } impl ObstacleAvoider { fn new() -> Result { Ok(Self { scan_in: Topic::new("scan")?, cmd_out: Topic::new("cmd_vel")?, }) } } impl Node for ObstacleAvoider { fn name(&self) -> &str { "obstacle_avoider" } fn tick(&mut self) { if let Some(scan) = self.scan_in.recv() { let min_range = scan.ranges.iter() .copied() .filter(|r| *r > scan.range_min && *r < scan.range_max) .fold(f32::MAX, f32::min); let speed = if min_range < 0.5 { 0.0 } else { 1.0 }; self.cmd_out.send(CmdVel { linear: speed, angular: 0.0 }); } } } ``` ## Communication Patterns **One-to-many** — one publisher, multiple subscribers. Each subscriber gets every message independently: ```rust // Publisher node let status = Topic::::new("status")?; status.send(Heartbeat { stamp: now() }); // Subscriber A and B both receive the heartbeat let status_a = Topic::::new("status")?; let status_b = Topic::::new("status")?; ``` **Many-to-one** — multiple publishers write to the same topic. The subscriber sees all messages interleaved: ```rust // Motor 1 and Motor 2 both publish to "motor.feedback" let fb1 = Topic::::new("motor.feedback")?; let fb2 = Topic::::new("motor.feedback")?; // Aggregator reads all feedback let all_fb = Topic::::new("motor.feedback")?; while let Some(fb) = all_fb.recv() { println!("Motor {} velocity: {}", fb.motor_id, fb.velocity); } ``` ## Monitoring Check topic health at runtime: ```rust let topic = Topic::::new("cmd_vel")?; if topic.has_message() { println!("{} messages pending", topic.pending_count()); } if topic.dropped_count() > 0 { hlog!(warn, "Dropped {} messages", topic.dropped_count()); } let m = topic.metrics(); println!("Sent: {}, Received: {}", m.messages_sent(), m.messages_received()); ``` ## See Also - [Topic API Reference](/rust/api/topic) — Full method reference - [Message Performance](/concepts/core-concepts-podtopic) — How HORUS optimizes message transfer - [Communication Overview](/concepts/communication-overview) — Topics vs services vs actions - [message! Macro](/rust/api/macros#message) — Custom message type reference ======================================== # SECTION: Python ======================================== --- ## Async Nodes Path: /python/api/async-nodes Description: Python async/await support for non-blocking HORUS nodes # Async Nodes HORUS automatically detects `async def` tick functions and runs them on the async I/O thread pool. No special classes or imports needed — just pass an async function to `Node()`. ## Basic Usage ```python import horus import aiohttp async def fetch_weather(node): async with aiohttp.ClientSession() as session: async with session.get("https://api.weather.com/data") as resp: data = await resp.json() node.send("weather", data) node = horus.Node( name="weather", tick=fetch_weather, rate=1, pubs=["weather"], ) horus.run(node) ``` That's it. `async def` is auto-detected — the scheduler runs this node on the async I/O thread pool (matching Rust's `.async_io()` execution class), so it doesn't block other nodes. ## How It Works When you pass an `async def` to `Node(tick=...)`: 1. `Node()` detects that `tick` is an async function 2. The scheduler automatically applies the async I/O execution class (matching Rust `.async_io()`) 3. The Rust scheduler runs this node on a separate thread pool, not the main tick thread Other (sync) nodes continue ticking while async nodes await. ## Async Init and Shutdown `init` and `shutdown` callbacks can also be async: ```python import horus import asyncpg async def setup(node): node.db = await asyncpg.connect("postgresql://localhost/robotics") async def process(node): if node.has_msg("data"): data = node.recv("data") await node.db.execute("INSERT INTO logs (value) VALUES ($1)", data) async def cleanup(node): await node.db.close() node = horus.Node( name="db_logger", tick=process, init=setup, shutdown=cleanup, rate=10, subs=["data"], ) horus.run(node) ``` ## Complete Example: HTTP API + Database ```python import horus import aiohttp import asyncpg async def fetch(node): """Fetch sensor data from HTTP API""" async with aiohttp.ClientSession() as session: async with session.get("https://api.example.com/sensor") as resp: if resp.status == 200: data = await resp.json() node.send("sensor.data", data) async def store_init(node): node.db = await asyncpg.connect("postgresql://localhost/robotics") async def store(node): """Store received data in database""" if node.has_msg("sensor.data"): data = node.recv("sensor.data") await node.db.execute( "INSERT INTO sensor_log (temp, humidity) VALUES ($1, $2)", data["temperature"], data["humidity"] ) async def store_shutdown(node): await node.db.close() horus.run( horus.Node(name="fetcher", tick=fetch, rate=1, pubs=["sensor.data"], order=0), horus.Node(name="storer", tick=store, init=store_init, shutdown=store_shutdown, rate=10, subs=["sensor.data"], order=1), ) ``` ## Mixing Sync and Async Sync and async nodes work together in the same scheduler. Sync nodes run on the main tick thread, async nodes run on the I/O thread pool: ```python import horus def read_sensor(node): """Fast sync sensor read""" node.send("raw", get_lidar_data()) async def upload(node): """Slow async cloud upload""" if node.has_msg("raw"): data = node.recv("raw") await cloud_client.upload(data) horus.run( horus.Node(name="sensor", tick=read_sensor, rate=100, order=0, pubs=["raw"]), horus.Node(name="upload", tick=upload, rate=1, order=1, subs=["raw"]), ) ``` No special handling — the scheduler detects which is async and routes accordingly. ## When to Use Async **Good use cases:** - HTTP/REST API integration (aiohttp, httpx) - Database operations (asyncpg, aioredis, motor) - WebSocket connections - File I/O operations - Any I/O-bound work that benefits from `await` **Not ideal for:** - CPU-bound computation → use `compute=True` instead - Real-time control loops → use sync tick with `budget` and `deadline` - Operations requiring <1ms latency → async overhead is ~1ms ## See Also - [Python Bindings](/python/api/python-bindings) — Core Python API - [ML Utilities](/python/library/ml-utilities) — ML inference helpers - [Examples](/python/examples) — More Python examples --- ## Python Examples Path: /python/examples Description: Python code examples for HORUS robotics applications # Python Examples All examples use the standard `horus.Node` callback API — one pattern, no inheritance. ```python import horus def my_tick(node): node.send("output", data) node = horus.Node(name="my_node", pubs=["output"], tick=my_tick, rate=30) horus.run(node) ``` --- ## Basic Node A minimal node that reads sensor data and publishes motor commands: ```python import horus def controller(node): """Simple obstacle avoidance""" if node.has_msg("sensor.distance"): distance = node.recv("sensor.distance") if distance < 0.5: node.send("motor.cmd", {"linear": 0.0, "angular": 0.5}) else: node.send("motor.cmd", {"linear": 1.0, "angular": 0.0}) node = horus.Node( name="obstacle_avoider", subs=["sensor.distance"], pubs=["motor.cmd"], tick=controller, rate=10 ) horus.run(node) ``` --- ## Typed Topics Using typed messages for proper logging and cross-language compatibility: ```python import horus def controller(node): if node.has_msg("localization.pose"): pose = node.recv("localization.pose") # Logs show: Pose2D { x: 2.31, y: 1.31, theta: 0.5 } cmd = horus.CmdVel(linear=1.0, angular=0.0) node.send("control.cmd", cmd) node = horus.Node( name="controller", subs={"localization.pose": {"type": horus.Pose2D}}, pubs={"control.cmd": {"type": horus.CmdVel}}, tick=controller, rate=30 ) horus.run(node) ``` Available typed messages: `CmdVel`, `Pose2D`, `Imu`, `Odometry`, `LaserScan`, `JointState`, `MotorCommand`, and 50+ more. --- ## Multi-Node System Multiple nodes with execution order: ```python import horus def sensor_tick(node): node.send("sensor.distance", 1.5) def controller_tick(node): if node.has_msg("sensor.distance"): dist = node.recv("sensor.distance") if dist < 0.5: node.send("cmd_vel", horus.CmdVel(linear=0.0, angular=0.5)) else: node.send("cmd_vel", horus.CmdVel(linear=1.0, angular=0.0)) def logger_tick(node): if node.has_msg("cmd_vel"): cmd = node.recv("cmd_vel") print(f"Command: linear={cmd.linear:.1f} angular={cmd.angular:.1f}") sensor = horus.Node(name="sensor", pubs=["sensor.distance"], tick=sensor_tick, rate=30) controller = horus.Node( name="controller", subs=["sensor.distance"], pubs={"cmd_vel": {"type": horus.CmdVel}}, tick=controller_tick, rate=30 ) logger = horus.Node(name="logger", subs=["cmd_vel"], tick=logger_tick, rate=10) # Quick run — no scheduler needed horus.run(sensor, controller, logger, duration=10.0) ``` --- ## Scheduler with Execution Control When you need execution order, failure policies, or RT features: ```python import horus sensor = horus.Node(name="sensor", pubs=["scan"], tick=sensor_tick, rate=100, order=0) controller = horus.Node(name="ctrl", subs=["scan"], pubs=["cmd"], tick=ctrl_tick, rate=100, order=1) logger = horus.Node(name="logger", subs=["cmd"], tick=log_tick, rate=10, order=10) scheduler = horus.Scheduler(tick_rate=100, watchdog_ms=500) # Critical — runs first scheduler.add(sensor) # RT control — runs after sensor scheduler.add(controller) # Non-critical — runs last scheduler.add(logger) scheduler.run() ``` ### Context Manager ```python sensor = horus.Node(name="sensor", tick=sensor_tick, rate=100, order=0) controller = horus.Node(name="ctrl", tick=ctrl_tick, rate=100, order=1) with horus.Scheduler(tick_rate=100) as sched: sched.add(sensor) sched.add(controller) sched.run(duration=30.0) # auto-stop on exit ``` ### Advanced Node Configuration For execution classes (compute, async I/O, event-driven), configure via Node kwargs: ```python scheduler = horus.Scheduler(tick_rate=500, rt=True) # Event-triggered: ticks when "lidar_scan" topic receives data scheduler.add(horus.Node(tick=detect_fn, on="lidar_scan", order=0)) # Compute pool: CPU-bound work on separate thread pool scheduler.add(horus.Node(tick=plan_fn, compute=True, rate=10)) # Async I/O: non-blocking network/file operations scheduler.add(horus.Node(tick=upload_fn, rate=1, failure_policy="ignore")) scheduler.run() ``` --- ## Sensor Processing Pipeline Processing laser scans for obstacle avoidance: ```python import horus def obstacle_avoidance(node): if node.has_msg("scan"): scan = node.recv("scan") if scan and hasattr(scan, 'ranges') and scan.ranges: min_dist = min(r for r in scan.ranges if r > 0.01) if min_dist < 0.5: node.send("cmd_vel", horus.CmdVel(linear=0.0, angular=0.5)) else: node.send("cmd_vel", horus.CmdVel(linear=1.0, angular=0.0)) node = horus.Node( name="obstacle_avoider", subs={"scan": {"type": horus.LaserScan}}, pubs={"cmd_vel": {"type": horus.CmdVel}}, tick=obstacle_avoidance, rate=10 ) horus.run(node) ``` --- ## Camera Image Pipeline Send and receive images with zero-copy shared memory: ```python import horus import numpy as np # Create image backed by shared memory img = horus.Image(480, 640, "rgb8") # Fill from NumPy (zero-copy when possible) pixels = np.zeros((480, 640, 3), dtype=np.uint8) pixels[:, :, 2] = 255 # Blue img = horus.Image.from_numpy(pixels) # Send over topic topic = horus.Topic("camera.rgb") topic.send(img) # Receive (in another node or process) received = topic.recv() if received is not None: arr = received.to_numpy() # zero-copy NumPy tensor = received.to_torch() # zero-copy PyTorch via DLPack print(f"Received {arr.shape[1]}x{arr.shape[0]} image") ``` --- ## Cross-Language Communication Python and Rust nodes communicate via shared memory topics — same machine, automatic: **Python publisher:** ```python import horus import time topic = horus.Topic(horus.CmdVel) while True: topic.send(horus.CmdVel(linear=1.0, angular=0.2)) time.sleep(0.1) ``` **Rust subscriber (separate process):** ```rust use horus::prelude::*; let topic: Topic = Topic::new("cmd_vel")?; loop { if let Some(cmd) = topic.recv() { println!("linear={}, angular={}", cmd.linear, cmd.angular); } } ``` --- ## Time API Framework clock for deterministic-compatible code: ```python import horus def physics_tick(node): dt = horus.dt() # Fixed in deterministic mode elapsed = horus.elapsed() # Time since start tick = horus.tick() # Tick number rng = horus.rng_float() # Deterministic random [0, 1) budget = horus.budget_remaining() # Seconds left in budget # Physics integration node.velocity += node.acceleration * dt node.position += node.velocity * dt node = horus.Node(name="physics", tick=physics_tick, rate=100) horus.run(node) ``` | Function | Returns | Description | |----------|---------|-------------| | `horus.now()` | `float` | Current time (seconds) | | `horus.dt()` | `float` | Timestep (seconds) | | `horus.elapsed()` | `float` | Time since start (seconds) | | `horus.tick()` | `int` | Current tick number | | `horus.budget_remaining()` | `float` | Budget left (seconds, `inf` if none) | | `horus.rng_float()` | `float` | Random in [0, 1) | | `horus.timestamp_ns()` | `int` | Nanosecond timestamp | --- ## Async Node Nodes with `async def` tick functions run on the async I/O thread pool — perfect for network requests and file I/O: ```python import horus async def fetch_weather(node): """Fetch weather data from an API every tick""" import aiohttp async with aiohttp.ClientSession() as session: async with session.get("http://api.weather.local/current") as resp: data = await resp.json() node.send("weather", data) weather = horus.Node( name="weather_fetcher", pubs=["weather"], tick=fetch_weather, # async def auto-detected rate=1 # 1 Hz — fetch every second ) horus.run(weather) ``` HORUS auto-detects `async def` and runs it on the async executor. No manual event loop setup needed. --- ## ML Inference ONNX model inference with performance monitoring: ```python import horus import numpy as np def detect_tick(node): if node.has_msg("camera.rgb"): img = node.recv("camera.rgb") arr = img.to_numpy() # Preprocess input_data = preprocess(arr) # Run inference results = model.run(None, {"input": input_data})[0] # Publish detections for det in parse_detections(results): node.send("detections", det) node = horus.Node( name="detector", subs={"camera.rgb": {"type": horus.Image}}, pubs=["detections"], tick=detect_tick, rate=30, compute=True, order=5 ) scheduler = horus.Scheduler(tick_rate=30) scheduler.add(node) # Runs on compute pool scheduler.run() ``` --- ## See Also - [Python Bindings](/python/api/python-bindings) - Core Python API - [Message Library](/python/library/python-message-library) - Available message types - [Time API (Rust reference)](/rust/time-api) - Full time API documentation - [Deterministic Mode](/advanced/deterministic-mode) - Deterministic execution guide --- ## ML Integration Path: /python/library/ml-utilities Description: Using PyTorch, ONNX Runtime, TensorFlow, and OpenCV with HORUS nodes # ML Integration Use ML frameworks directly in horus nodes — no wrapper library needed. Import PyTorch, ONNX Runtime, TensorFlow, or OpenCV and use them in your `tick` function. ## Zero-Copy Interop Matrix horus data types integrate with the Python ML ecosystem via three protocols: `__array_interface__` (NumPy), `__dlpack__` (universal), and `__cuda_array_interface__` (GPU). | horus type | NumPy | PyTorch | JAX | OpenCV | ONNX RT | |-----------|-------|---------|-----|--------|---------| | **Image** | `to_numpy()` / `from_numpy()` | `to_torch()` / `from_torch()` | `to_jax()` | via `to_numpy()` | via `to_numpy()` | | **PointCloud** | `to_numpy()` / `from_numpy()` | `to_torch()` / `from_torch()` | `to_jax()` | — | via `to_numpy()` | | **DepthImage** | `to_numpy()` / `from_numpy()` | `to_torch()` / `from_torch()` | `to_jax()` | via `to_numpy()` | via `to_numpy()` | All conversions are **zero-copy** (~3μs constant time, regardless of data size). The Python side gets a view into horus shared memory — no pixel data is copied. ```python img = node.recv("camera") # Any of these — all zero-copy, all ~3μs: np_arr = img.to_numpy() # NumPy ndarray tensor = img.to_torch() # PyTorch tensor jax_arr = img.to_jax() # JAX array dlpack = np.from_dlpack(img) # DLPack protocol (979ns) ``` **Performance**: A 1920×1080 RGB image (6MB) takes 3μs to access as NumPy vs 178μs to copy — **59x faster**. See [Benchmarks](/performance/benchmarks#python-benchmarks) for full numbers. ## ONNX Runtime (Recommended for Production) ```python import horus import onnxruntime as ort import numpy as np session = ort.InferenceSession("yolov8n.onnx", providers=["CUDAExecutionProvider"]) def detect(node): if node.has_msg("camera"): img = node.recv("camera").to_numpy() img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1))[np.newaxis] # HWC→NCHW output = session.run(None, {"images": img}) node.send("detections", output[0]) horus.run( horus.Node(tick=detect, rate=30, subs=["camera"], pubs=["detections"], order=0), ) ``` ## PyTorch ```python import horus import torch model = torch.jit.load("resnet50.pt", map_location="cuda:0") model.eval() def classify(node): if node.has_msg("camera"): img = node.recv("camera").to_torch() # Zero-copy to PyTorch tensor with torch.no_grad(): output = model(img.unsqueeze(0).cuda()) class_id = output.argmax(dim=1).item() node.send("class", {"id": class_id, "confidence": output.max().item()}) horus.run( horus.Node(tick=classify, rate=10, subs=["camera"], pubs=["class"]), ) ``` ## OpenCV ```python import horus import cv2 import numpy as np def process_frame(node): if node.has_msg("camera"): img = node.recv("camera").to_numpy() gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) edges = cv2.Canny(gray, 50, 150) result = horus.Image.from_numpy(edges) node.send("edges", result) horus.run( horus.Node(tick=process_frame, rate=30, subs=["camera"], pubs=["edges"]), ) ``` ## TensorFlow / TFLite ```python import horus import tensorflow as tf model = tf.saved_model.load("saved_model") def infer(node): if node.has_msg("input"): data = node.recv("input") tensor = tf.convert_to_tensor(data, dtype=tf.float32) output = model(tensor) node.send("output", output.numpy()) horus.run(horus.Node(tick=infer, rate=10, subs=["input"], pubs=["output"])) ``` ## Performance Tips - **Use `compute=True`** for CPU-bound inference — runs on thread pool, releases GIL during C extension calls (NumPy, ONNX, PyTorch): ```python horus.Node(tick=detect, rate=30, compute=True, ...) ``` - **Set realistic `budget`** to detect slow inference: ```python horus.Node(tick=detect, rate=30, budget=50 * horus.ms, on_miss="skip") ``` - **Use `horus.Image.to_torch()`** for zero-copy GPU transfer — no pixel data copied. - **Batch with `recv_all()`** if messages queue up: ```python def batch_infer(node): frames = node.recv_all("camera") if frames: batch = np.stack([f.to_numpy() for f in frames]) outputs = session.run(None, {"images": batch}) for det in outputs[0]: node.send("detections", det) ``` ## See Also - [Memory Types](/python/api/memory-types) — Image, PointCloud zero-copy interop - [Async Nodes](/python/api/async-nodes) — Non-blocking inference with `async def` - [Perception Types](/python/api/perception) — Detection, BoundingBox, Landmark types --- ## Python Bindings Path: /python/api/python-bindings Description: Production-ready Python API for HORUS robotics with advanced features # HORUS Python Bindings **Production-Ready Python API** for the HORUS robotics framework - combines simplicity with advanced features for professional robotics applications. ## Why HORUS Python? - **Zero Boilerplate**: Working node in 10 lines - **Flexible API**: Functional style or class inheritance - your choice - **Production Performance**: ~500ns latency (same shared memory as Rust) - **Per-Node Rate Control**: Different nodes at different frequencies (100Hz sensor, 10Hz logger) - **Message Timestamps**: Typed messages include `timestamp_ns` for timing - **Typed Messages**: Optional type-safe messages from Rust - **Multiprocess Support**: Process isolation and multi-language nodes - **Pythonic**: Feels like native Python, not a foreign function wrapper - **Rich Ecosystem**: Use NumPy, OpenCV, scikit-learn, etc. --- ## Quick Start ### Installation **Automatic (Recommended)** Python bindings are automatically installed when you run the HORUS installer: ```bash # From HORUS root directory ./install.sh ``` The installer will detect Python 3.9+ and automatically build and install the bindings. **Manual Installation** If you prefer to install manually or need to rebuild: ```bash # Install maturin (Python/Rust build tool) # Option A: Via Cargo (recommended for Ubuntu 24.04+) cargo install maturin # Option B: Via pip (if not blocked by PEP 668) # pip install maturin # Build and install from source cd horus_py maturin develop --release ``` **Requirements**: - Python 3.9+ - Rust 1.70+ - Linux (for shared memory support) ### Minimal Example ```python import horus def process(node): node.send("output", "Hello HORUS!") node = horus.Node(pubs="output", tick=process, rate=1) horus.run(node, duration=3) ``` This minimal example demonstrates functional-style node creation without class boilerplate. --- ## Core API ### Creating a Node ```python def Node( name: str = "", # Node name (auto-generated from tick function if empty) subs: str | list = "", # Topics to subscribe (string, list of strings, or list of message types) pubs: str | list = "", # Topics to publish (string, list of strings, or list of message types) tick: Callable = None, # Function called every tick: tick(node) -> None init: Callable = None, # Optional: called once on startup: init(node) -> None shutdown: Callable = None, # Optional: called on graceful shutdown: shutdown(node) -> None rate: int = 30, # Tick rate in Hz order: int = 100, # Execution priority (lower = earlier) budget: float = None, # Max tick duration in seconds (None = auto from rate) deadline: float = None, # Hard deadline in seconds (None = auto from rate) on_miss: str = None, # "warn", "skip", "safe_mode", "stop" (None = "warn") ) -> Node ``` **Parameters:** - `name` — Node identifier. If empty, derived from tick function name. - `subs/pubs` — Topic declarations. Accepts: `"topic_name"`, `["topic1", "topic2"]`, or typed `[CmdVel, LaserScan]` (see formats below). - `tick` — Main loop function, called at `rate` Hz. Receives the node instance. - `rate` — Tick frequency in Hz. Default: 30. Setting rate auto-enables RT scheduling. - `order` — Priority within scheduler tick (0-9: critical, 10-49: sensors, 50-99: processing, 100+: background). **Example:** ```python from horus import Node, CmdVel, LaserScan, Imu node = Node( name="controller", pubs=[CmdVel], # Typed — fast Pod zero-copy (~1.5μs) subs=[LaserScan, Imu], # Auto-named: "scan", "imu" tick=control_fn, rate=100, order=0, ) ``` **Topic declaration formats** (determines performance path): ```python # FAST — typed (Pod zero-copy, ~2.7μs send+recv) pubs=[CmdVel] # auto-name from type: "cmd_vel" pubs=[CmdVel, Pose2D] # multiple typed topics pubs={"motor": CmdVel} # custom name + type # GENERIC — string (MessagePack, ~10μs send+recv) pubs=["data"] # GenericMessage — for dicts and custom data pubs="single_topic" # shorthand for single string topic ``` **Parameters**: - `name` (str, optional): Node name (auto-generated if omitted) - `pubs`: Topics to publish — `[CmdVel]` (typed, fast) or `["name"]` (generic) - `subs`: Topics to subscribe — same formats as pubs - `tick` (callable): Function called each cycle, receives `(node)` as argument - `rate` (float): Execution rate in Hz (default: 30) - `init` (callable, optional): Setup function, called once at start - `shutdown` (callable, optional): Cleanup function, called once at end - `on_error` (callable, optional): Error handler, called if tick raises an exception - `default_capacity` (int, optional): Buffer capacity for auto-created topics (default: 1024) ### Alternative: Class as State Container For nodes with complex state, use a plain class and pass its method as the tick function: ```python import horus class SensorState: def __init__(self): self.reading = 0.0 def tick(self, node): self.reading += 0.1 node.send("temperature", self.reading) def init(self, node): print("Sensor initialized!") def shutdown(self, node): print("Sensor shutting down!") # Use it state = SensorState() sensor = horus.Node(name="sensor", tick=state.tick, init=state.init, shutdown=state.shutdown, pubs=["temperature"], rate=10) horus.run(sensor) ``` **Both patterns work!** Use functional style for simplicity or class containers for complex nodes with state. ### Node Functions Your tick function receives the node as a parameter: ```python def my_tick(node): # Check for messages if node.has_msg("input"): data = node.recv("input") # Get one message # Get all messages all_msgs = node.recv_all("input") # Send messages node.send("output", {"value": 42}) ``` **Node Methods**: #### send ```python def send(topic: str, data: Any) -> None ``` Publish a message to a topic. Non-blocking. Overwrites oldest if buffer is full. **Parameters:** - `topic: str` — Topic name (must be in the node's `pubs` list) - `data: Any` — Message to send. Can be: a dict, a typed message (`CmdVel`, `Image`, etc.), or any serializable object ```python node.send("cmd_vel", {"linear": 1.0, "angular": 0.0}) # dict node.send("cmd_vel", horus.CmdVel(1.0, 0.0)) # typed message ``` #### recv ```python def recv(topic: str) -> Optional[Any] ``` Receive one message from a topic (FIFO order). Returns `None` if no messages available. **Parameters:** - `topic: str` — Topic name (must be in the node's `subs` list) **Returns:** The message, or `None` if buffer is empty. ```python msg = node.recv("scan") if msg is not None: print(f"Got {len(msg.ranges)} ranges") ``` #### `node.recv_all(topic) -> list` Receive ALL available messages as a list. Drains the buffer completely. Returns an empty list if none available. Use this for batch processing when you need to handle every message, not just the latest: ```python def tick(node): # Process all queued commands (don't drop any) commands = node.recv_all("commands") for cmd in commands: execute_command(cmd) node.log_debug(f"Processed {len(commands)} commands this tick") ``` #### `node.has_msg(topic) -> bool` Check if at least one unread message is available on the topic without consuming it. The message is buffered internally and returned by the next `recv()` call. ```python def tick(node): if node.has_msg("emergency_stop"): stop = node.recv("emergency_stop") node.log_warning("Emergency stop received!") node.request_stop() ``` #### `node.request_stop()` Request the scheduler to shut down gracefully after the current tick completes. Use this to stop execution programmatically from within a node. ```python def tick(node): if horus.tick() >= 1000: node.log_info("Reached 1000 ticks, stopping") node.request_stop() error = check_safety() if error: node.log_error(f"Safety violation: {error}") node.request_stop() ``` #### `node.publishers() -> List[str]` Returns the list of topic names this node publishes to. ```python def init(node): node.log_info(f"Publishing to: {node.publishers()}") node.log_info(f"Subscribing to: {node.subscribers()}") ``` #### `node.subscribers() -> List[str]` Returns the list of topic names this node subscribes to. #### Logging Methods | Method | Description | |--------|-------------| | `node.log_info(msg)` | Log an informational message | | `node.log_warning(msg)` | Log a warning message | | `node.log_error(msg)` | Log an error message | | `node.log_debug(msg)` | Log a debug message | **Important:** Logging only works during `init()`, `tick()`, or `shutdown()` callbacks. Calling outside the scheduler raises a `RuntimeWarning` and the message is silently dropped. ```python def tick(node): node.log_info("Processing sensor data") node.log_warning("Sensor reading is stale") node.log_error("Failed to process data") node.log_debug(f"Raw value: {value}") # Outside scheduler — message is dropped with RuntimeWarning: node = horus.Node(name="test", tick=tick) node.log_info("This will be dropped!") # RuntimeWarning ``` **Node scheduling kwargs** (maps 1:1 to Rust NodeBuilder): | Kwarg | Default | Rust equivalent | |-------|---------|-----------------| | `rate` | `30` | `.rate()` | | `order` | `100` | `.order()` | | `budget` | `None` | `.budget()` | | `deadline` | `None` | `.deadline()` | | `on_miss` | `None` (warn) | `.on_miss()` | | `failure_policy` | `None` (fatal) | `.failure_policy()` | | `compute` | `False` | `.compute()` | | `on` | `None` | `.on(topic)` | | `priority` | `None` | `.priority()` | | `core` | `None` | `.core()` | | `watchdog` | `None` | `.watchdog()` | | async `tick` | auto-detected | `.async_io()` | :::tip Python Timing Guide **Budget/deadline** in Python detect overruns, not guarantee timing. Python ticks take milliseconds, not microseconds. Use realistic values: ```python # Rust node: microsecond budget Node(tick=motor_ctrl, rate=1000, budget=300 * us) # 300μs — Rust can do this # Python ML node: millisecond budget — detects when inference is too slow Node(tick=run_model, rate=30, budget=50 * ms) # 50ms — triggers on_miss if exceeded ``` **compute=True** is useful when your tick calls C extensions that release the GIL (NumPy, PyTorch, OpenCV) — they run in parallel on the thread pool. **priority/core** are for mixed Rust+Python systems — tell the OS to schedule Rust RT nodes before Python, and keep Python off RT cores. ::: ### Running Nodes ```python def run(*nodes: Node, duration: float = None) -> None ``` Convenience one-liner: creates a Scheduler, adds all nodes, and runs. **Parameters:** - `*nodes: Node` — One or more Node instances to run - `duration: float` — Optional. Run for this many seconds, then stop. `None` = run until Ctrl+C. ```python # Single node — runs until Ctrl+C horus.run(node) # Multiple nodes for 10 seconds horus.run(node1, node2, node3, duration=10) ``` --- ## Examples ### 1. Simple Publisher ```python import horus def publish_temperature(node): node.send("temperature", 25.5) sensor = horus.Node( name="temp_sensor", pubs="temperature", tick=publish_temperature, rate=1 # 1 Hz ) horus.run(sensor, duration=10) ``` ### 2. Subscriber ```python import horus def display_temperature(node): if node.has_msg("temperature"): temp = node.recv("temperature") print(f"Temperature: {temp}°C") display = horus.Node( name="display", subs="temperature", tick=display_temperature ) horus.run(display) ``` ### 3. Pub/Sub Pipeline ```python import horus def publish(node): node.send("raw", 42.0) def process(node): if node.has_msg("raw"): data = node.recv("raw") result = data * 2.0 node.send("processed", result) def display(node): if node.has_msg("processed"): value = node.recv("processed") print(f"Result: {value}") # Create pipeline publisher = horus.Node("publisher", pubs="raw", tick=publish, rate=1) processor = horus.Node("processor", subs="raw", pubs="processed", tick=process) displayer = horus.Node("display", subs="processed", tick=display) # Run all together horus.run(publisher, processor, displayer, duration=5) ``` ### 4. Using Lambda Functions ```python import horus # Producer (inline) producer = horus.Node( pubs="numbers", tick=lambda n: n.send("numbers", 42), rate=1 ) # Transformer (inline) doubler = horus.Node( subs="numbers", pubs="doubled", tick=lambda n: n.send("doubled", n.get("numbers") * 2) if n.has_msg("numbers") else None ) horus.run(producer, doubler, duration=5) ``` ### 5. Multi-Topic Robot Controller ```python import horus def robot_controller(node): # Read from multiple sensors lidar_data = None camera_data = None if node.has_msg("lidar"): lidar_data = node.recv("lidar") if node.has_msg("camera"): camera_data = node.recv("camera") # Compute commands if lidar_data and camera_data: cmd = compute_navigation(lidar_data, camera_data) node.send("motors", cmd) node.send("status", "navigating") robot = horus.Node( name="robot_controller", subs=["lidar", "camera"], pubs=["motors", "status"], tick=robot_controller, rate=50 # 50Hz control loop ) ``` ### 6. Lifecycle Management ```python import horus class Context: def __init__(self): self.count = 0 self.file = None ctx = Context() def init_handler(node): print("Starting up!") ctx.file = open("data.txt", "w") def tick_handler(node): ctx.count += 1 data = f"Tick {ctx.count}" node.send("data", data) ctx.file.write(data + "\n") def shutdown_handler(node): print(f"Processed {ctx.count} messages") ctx.file.close() node = horus.Node( pubs="data", init=init_handler, tick=tick_handler, shutdown=shutdown_handler, rate=10 ) horus.run(node, duration=5) ``` --- ## Advanced Features (Production-Ready) HORUS Python includes advanced features that match or exceed ROS2 capabilities while maintaining simplicity. ### NodeState The `NodeState` enum tracks which lifecycle phase a node is in: ```python from horus import NodeState # Values: NodeState.IDLE # Created but not yet running NodeState.RUNNING # Actively ticking NodeState.PAUSED # Temporarily suspended NodeState.STOPPED # Clean shutdown complete NodeState.ERROR # Error state ``` NodeState values are strings — you can compare directly: ```python if node_state == "running": print("Node is active") ``` ### Scheduler ```python def Scheduler( tick_rate: int = 100, # Global tick rate in Hz rt: bool = False, # Enable real-time scheduling (prefer_rt) watchdog_ms: int = 0, # Watchdog timeout in ms (0 = disabled) deterministic: bool = False, # Deterministic execution mode verbose: bool = True, # Enable verbose logging ) -> Scheduler ``` The Scheduler orchestrates node execution with priority ordering, per-node rate control, and real-time features. **Methods:** - `scheduler.add(node: Node) -> None` — Register a node - `scheduler.run() -> None` — Start the main loop (blocks until Ctrl+C) - `scheduler.run_for(seconds: float) -> None` — Run for a duration, then stop - `scheduler.tick_once() -> None` — Execute one tick cycle (for testing) - `scheduler.stop() -> None` — Request graceful shutdown **Creating a Scheduler:** ```python # All config on Node(), scheduler.add() takes only the node scheduler = horus.Scheduler() scheduler.add(horus.Node(tick=motor_fn, rate=1000, order=0, budget=200)) scheduler.add(horus.Node(tick=planner_fn, order=5, compute=True)) scheduler.add(horus.Node(tick=telemetry_fn, rate=1, order=10)) ``` **Node configuration** (kwargs on `Node()`): | Method | Description | |--------|-------------| | `.order(n)` | Execution priority (lower = runs first) | | `.rate(hz)` | Node tick rate in Hz — auto-derives budget/deadline, marks as RT | | `.budget(us)` | Tick budget in microseconds | | `.on_miss(policy)` | `"warn"`, `"skip"`, `"safe_mode"`, or `"stop"` | | `.on(topic)` | Event-driven — wakes only when topic has new data | | `.compute()` | Offload to worker thread pool (planning, ML) | | `.async_io()` | Run on async executor (network, disk) | | `.failure_policy(name, ...)` | `"fatal"`, `"restart"`, `"skip"`, or `"ignore"` — optional kwargs: `max_retries`, `backoff_ms`, `max_failures`, `cooldown_ms` | | `.build()` | Finalize and register — returns `Scheduler` | **Adding Nodes:** All configuration (order, rate, budget, etc.) goes on the `Node()` constructor. `scheduler.add()` takes only the node: ```python sensor = horus.Node(name="sensor", tick=sensor_fn, rate=100, order=0) controller = horus.Node(name="ctrl", tick=ctrl_fn, rate=100, order=1) logger = horus.Node(name="logger", tick=log_fn, rate=10, order=2) scheduler.add(sensor) scheduler.add(controller) scheduler.add(logger) ``` **Execution:** | Method | Description | |--------|-------------| | `scheduler.run()` | Run until Ctrl+C or `.stop()` | | `scheduler.run(duration=10.0)` | Run for a specific duration, then shut down | | `scheduler.stop()` | Signal graceful shutdown | | `scheduler.current_tick()` | Current tick count | **Monitoring:** | Method | Description | |--------|-------------| | `scheduler.get_node_stats(name)` | Stats dict: `total_ticks`, `errors_count`, `avg_tick_duration_ms`, etc. | | `scheduler.set_node_rate(name, rate)` | Change a node's tick rate at runtime | | `scheduler.set_tick_budget(name, us)` | Update per-node tick budget (microseconds) | | `scheduler.get_all_nodes()` | List all nodes with their configuration | | `scheduler.get_node_count()` | Number of registered nodes | | `scheduler.has_node(name)` | Check if a node is registered | | `scheduler.get_node_names()` | List of registered node names | | `scheduler.remove_node(name)` | Remove a node (returns `True` if found) | | `scheduler.status()` | Formatted status string | | `scheduler.capabilities()` | Dict of RT capabilities | | `scheduler.has_full_rt()` | `True` if all RT features available | | `scheduler.safety_stats()` | Dict of budget overruns, deadline misses, watchdog expirations | **Recording & Replay:** | Method | Description | |--------|-------------| | `scheduler.is_recording()` | Check if recording is active | | `scheduler.is_replaying()` | Check if replaying | | `scheduler.stop_recording()` | Stop recording, returns list of saved file paths | | `Scheduler.list_recordings()` | List available recordings (static method) | | `Scheduler.delete_recording(name)` | Delete a recording (static method) | **Context Manager:** The Scheduler supports the `with` statement for automatic cleanup: ```python with horus.Scheduler(tick_rate=100) as sched: sched.add(horus.Node(tick=sensor_fn, rate=100, order=0)) sched.add(horus.Node(tick=ctrl_fn, rate=100, order=1)) sched.run(duration=10.0) # stop() called automatically on exit, even if an exception occurs ``` **Expanded Method Details:** #### `scheduler.get_node_stats(name) -> dict` Returns a dictionary with detailed statistics for the named node: ```python stats = scheduler.get_node_stats("motor_ctrl") print(f"Total ticks: {stats['total_ticks']}") print(f"Avg tick: {stats.get('avg_tick_duration_ms', 0):.2f} ms") print(f"Errors: {stats['errors_count']}") ``` Keys include: `name`, `priority`, `total_ticks`, `successful_ticks`, `failed_ticks`, `avg_tick_duration_ms`, `max_tick_duration_ms`, `errors_count`, `uptime_seconds`. #### `scheduler.status() -> str` Returns the current scheduler state: `"idle"`, `"running"`, or `"stopped"`. #### `scheduler.current_tick() -> int` Returns the current tick count (0-indexed). #### `scheduler.set_node_rate(name, rate)` Change a node's tick rate at runtime. Useful for adaptive control: ```python # Slow down logging when battery is low if battery_low: scheduler.set_node_rate("logger", 1) # 1 Hz else: scheduler.set_node_rate("logger", 10) # 10 Hz ``` **`horus.run()` — The ONE way to run nodes:** ```python from horus import Node, run, us sensor = Node(tick=read_lidar, rate=10, order=0, pubs=["scan"]) ctrl = Node(tick=navigate, rate=30, order=1, subs=["scan"], pubs=["cmd"]) motor = Node(tick=drive, rate=1000, order=2, budget=300*us, subs=["cmd"]) # All scheduler config as kwargs run(sensor, ctrl, motor, rt=True, watchdog_ms=500) run(node, duration=10, deterministic=True) ``` **All `run()` kwargs** (maps to Rust Scheduler builder): | Kwarg | Default | Rust equivalent | |-------|---------|-----------------| | `duration` | `None` (forever) | `.run()` / `.run_for()` | | `tick_rate` | `1000.0` | `.tick_rate()` | | `rt` | `False` | `.prefer_rt()` | | `deterministic` | `False` | `.deterministic()` | | `watchdog_ms` | `0` | `.watchdog()` | | `blackbox_mb` | `0` | `.blackbox()` | | `recording` | `False` | `.with_recording()` | | `name` | `None` | `.name()` | | `cores` | `None` | `.cores()` | | `max_deadline_misses` | `None` | `.max_deadline_misses()` | | `verbose` | `False` | `.verbose()` | | `telemetry` | `None` | `.telemetry()` | ### Miss — Deadline Miss Policy The `Miss` class defines what happens when a node exceeds its deadline: ```python from horus import Miss # Available policies Miss.WARN # Log warning and continue (default) Miss.SKIP # Skip the node for this tick Miss.SAFE_MODE # Call enter_safe_state() on the node Miss.STOP # Stop the entire scheduler ``` Use via the Node constructor: ```python # Config on Node, then add motor = horus.Node(name="motor", tick=motor_fn, rate=500, order=0, budget=200, on_miss="safe_mode") scheduler.add(motor) ``` ### Scheduler Configuration All configuration via constructor kwargs: ```python from horus import Scheduler, Node # Development — simple scheduler = Scheduler() # Production — watchdog + RT scheduler = Scheduler(tick_rate=1000, rt=True, watchdog_ms=500) # With blackbox + telemetry scheduler = Scheduler( tick_rate=1000, watchdog_ms=500, blackbox_mb=64, telemetry="http://localhost:9090", verbose=True, ) # Deterministic mode for simulation/testing scheduler = Scheduler(tick_rate=100, deterministic=True) ``` **Testing with short runs:** ```python scheduler = Scheduler() scheduler.add(Node(name="sensor", tick=sensor_fn, rate=100, order=0)) scheduler.add(Node(name="ctrl", tick=ctrl_fn, rate=100, order=1)) # Run for a short duration scheduler.run(duration=0.1) ``` ### Message Timestamps Timestamps are managed by the Rust Topic backend. Typed messages include a `timestamp_ns` field for nanosecond-precision timing: ```python import horus import time def control_tick(node): if node.has_msg("sensor_data"): msg = node.recv("sensor_data") # Use message-level timestamps for latency checks if hasattr(msg, 'timestamp_ns') and msg.timestamp_ns: age_s = (time.time_ns() - msg.timestamp_ns) / 1e9 if age_s > 0.1: # More than 100ms old node.log_warning(f"Stale data: {age_s*1000:.1f}ms old") return latency = age_s print(f"Latency: {latency*1000:.1f}ms") # Process fresh data process(msg) ``` **Timestamp access:** Use `msg.timestamp_ns` on typed messages (CmdVel, Pose2D, Imu, etc.) for nanosecond timestamps set by the Rust backend. ### Multiprocess Execution Run Python nodes in separate processes for isolation and multi-language support: ```bash # Run multiple Python files as separate processes horus run node1.py node2.py node3.py # Mix Python and Rust nodes horus run sensor.rs controller.py visualizer.py # Mix Rust and Python horus run lidar_driver.rs planner.py motor_control.rs ``` All nodes in the same `horus run` session automatically communicate via shared memory! **Example - Distributed System:** ```python # sensor_node.py import horus def sensor_tick(node): data = read_lidar() # Your sensor code node.send("lidar_data", data) sensor = horus.Node(name="lidar", pubs="lidar_data", tick=sensor_tick) horus.run(sensor) ``` ```python # controller_node.py import horus def control_tick(node): if node.has_msg("lidar_data"): data = node.recv("lidar_data") cmd = compute_control(data) node.send("motor_cmd", cmd) controller = horus.Node( name="controller", subs="lidar_data", pubs="motor_cmd", tick=control_tick ) horus.run(controller) ``` ```bash # Run both in separate processes horus run sensor_node.py controller_node.py ``` **Benefits:** - **Process isolation**: One crash doesn't kill everything - **Multi-language**: Mix Python and Rust nodes in the same application - **Parallel execution**: True multicore utilization - **Zero configuration**: Shared memory IPC automatically set up ### Complete Example: All Features Together ```python import horus import time def sensor_tick(node): """High-frequency sensor (100Hz)""" imu = {"accel_x": 1.0, "accel_y": 0.0, "accel_z": 9.8} node.send("imu_data", imu) node.log_info("Published IMU data") def control_tick(node): """Medium-frequency control (50Hz)""" if node.has_msg("imu_data"): imu = node.recv("imu_data") cmd = {"linear": 1.0, "angular": 0.0} node.send("cmd_vel", cmd) def logger_tick(node): """Low-frequency logging (10Hz)""" if node.has_msg("cmd_vel"): msg = node.recv("cmd_vel") node.log_info(f"Command received: {msg}") # Create nodes with rate and order configured on the Node sensor = horus.Node(name="imu", pubs="imu_data", tick=sensor_tick, rate=100, order=0) controller = horus.Node(name="ctrl", subs="imu_data", pubs="cmd_vel", tick=control_tick, rate=50, order=1) logger = horus.Node(name="log", subs="cmd_vel", tick=logger_tick, rate=10, order=2) # Add nodes to scheduler scheduler = horus.Scheduler() scheduler.add(sensor) scheduler.add(controller) scheduler.add(logger) scheduler.run(duration=5.0) # Check statistics stats = scheduler.get_node_stats("imu") print(f"Sensor: {stats['total_ticks']} ticks in 5 seconds") ``` --- ## Network Communication HORUS Python supports network communication for distributed multi-machine systems. Topic, and Router all work transparently over the network. ### Topic Network Endpoints Add an `endpoint` parameter to communicate over the network: ```python from horus import Topic, CmdVel # Local (shared memory) - default local_topic = Topic(CmdVel) # Network (UDP direct) network_topic = Topic(CmdVel, endpoint="cmdvel@192.168.1.100:8000") # Router (TCP broker for WAN/NAT traversal) router_topic = Topic(CmdVel, endpoint="cmdvel@router") ``` **Endpoint Syntax:** - `"topic"` - Local shared memory (~500ns latency) - `"topic@host:port"` - Direct UDP (<50μs latency) - `"topic@router"` - Router broker (auto-discovery on localhost:7777) - `"topic@192.168.1.100:7777"` - Router broker at specific address ### Topic Methods | Method | Description | |--------|-------------| | `topic.send(msg, node=None)` | Send a message. Pass optional `node` for automatic IPC logging. Returns `True`. | | `topic.recv(node=None)` | Receive one message. Returns the message or `None` if empty. | | `topic.name` | Property: the topic name string | | `topic.backend_type` | Property: the active backend name (e.g., `"direct"`, `"spsc_shm"`) | | `topic.is_network_topic` | Property: `True` if this topic uses network transport | | `topic.endpoint` | Property: the endpoint string, or `None` for local topics | | `topic.stats()` | Returns a dict with `messages_sent`, `messages_received`, `send_failures`, `recv_failures`, `is_network`, `backend` | | `topic.is_generic()` | Returns `True` if this is a generic (string-name) topic | **Example:** ```python from horus import Topic, CmdVel topic = Topic(CmdVel) # Send and receive typed messages topic.send(CmdVel(linear=1.0, angular=0.5)) msg = topic.recv() if msg: print(f"linear={msg.linear}, angular={msg.angular}") # Check topic properties print(f"Name: {topic.name}") # "cmd_vel" print(f"Backend: {topic.backend_type}") # e.g. "mpmc_shm" print(f"Stats: {topic.stats()}") ``` ### Generic Topics When you create a Topic with a **string name** (instead of a typed class), you get a generic topic that accepts any JSON-serializable data: ```python from horus import Topic, CmdVel # Generic topic (string name = dynamic typing) topic = Topic("my_topic") # Typed topic (class = static typing, better performance) typed_topic = Topic(CmdVel) ``` Generic topics use the same `send()` and `recv()` methods as typed topics, but accept any JSON-serializable Python object. Data is serialized via MessagePack internally. ```python from horus import Topic topic = Topic("sensor_data") # Send dict, list, or any JSON-serializable data topic.send({"temperature": 25.5, "humidity": 60.0}) topic.send([1.0, 2.0, 3.0, 4.0]) topic.send("status: OK") # Receive (returns Python object) msg = topic.recv() # {"temperature": 25.5, "humidity": 60.0} # Check if generic print(topic.is_generic()) # True ``` **Typed vs Generic Performance:** | Topic Type | Serialization | Use Case | |------------|---------------|----------| | Typed (`Topic(CmdVel)`) | Direct field extraction (no serde) | Production, cross-language, high-frequency | | Generic (`Topic("name")`) | Python → JSON → MessagePack | Dynamic schemas, prototyping, Python-only | ### Automatic Transport Selection HORUS automatically selects the fastest communication path based on where publishers and subscribers are located. You never need to configure this manually: ```python from horus import Topic, CmdVel # Just create a topic — HORUS picks the fastest path automatically: # Same-thread: ~3ns (when pub+sub are in the same node) # Same-process: ~18-36ns (when pub+sub are in different nodes, same process) # Cross-process: ~85-167ns (when pub+sub are in different processes) topic = Topic("cmd_vel", CmdVel) ``` **Automatic Transport Tiers:** | Scenario | Latency | When it applies | |----------|---------|-----------------| | Same thread | ~3ns | Publisher and subscriber are in the same node | | Same process (1:1) | ~18ns | One publisher, one subscriber, same process | | Same process (many:1) | ~26ns | Multiple publishers, one subscriber, same process | | Same process (many:many) | ~36ns | Multiple publishers and subscribers, same process | | Cross-process (1:1) | ~85ns | One publisher, one subscriber, different processes | | Cross-process (many:many) | ~167ns | Multiple publishers and subscribers, different processes | ### Router Client (WAN/NAT Traversal) For communication across networks, through NAT, or for large-scale deployments, use the Router: ```python from horus import RouterClient, Topic, CmdVel # Create router client for explicit connection management router = RouterClient("192.168.1.100", 7777) # Build endpoints through the router cmd_endpoint = router.endpoint("cmdvel") # Returns "cmdvel@192.168.1.100:7777" pose_endpoint = router.endpoint("pose") # Use endpoints with Topic topic = Topic(CmdVel, endpoint=cmd_endpoint) # Router properties print(f"Address: {router.address}") # "192.168.1.100:7777" print(f"Connected: {router.is_connected}") # True print(f"Topics: {router.topics}") # ["cmdvel", "pose"] print(f"Uptime: {router.uptime_seconds}s") ``` **Helper Functions:** ```python from horus import default_router_endpoint, router_endpoint # Default router (localhost:7777) ep1 = default_router_endpoint("cmdvel") # "cmdvel@router" # Custom router address ep2 = router_endpoint("cmdvel", "192.168.1.100", 7777) # "cmdvel@192.168.1.100:7777" ``` **Router Server (for testing):** ```python from horus import RouterServer # Start a local router (for development/testing) server = RouterServer(port=7777) server.start() # For production, use CLI instead: # $ horus router start --port 7777 ``` ### When to Use What | Transport | Latency | Use Case | |-----------|---------|----------| | Same-process (`Topic(CmdVel)`) | ~18-36ns | In-process communication (automatic) | | Cross-process, 1:1 (`Topic(CmdVel)`) | ~85ns | Same machine, one publisher and one subscriber | | Cross-process, many:many (`Topic(CmdVel)`) | ~167ns | Same machine, multiple publishers and subscribers | | Network (`endpoint="topic@host:port"`) | <50μs | Multi-machine on LAN (direct UDP) | | Router (`endpoint="topic@router"`) | 10-50ms | WAN, NAT traversal, cloud deployments | ### Multi-Machine Example ```python # === ROBOT (192.168.1.50) === from horus import Topic, CmdVel, Imu, Odometry # Local: Critical flight control (ultra-fast) imu_topic = Topic(Imu) # ~85ns local shared memory # Network: Telemetry to ground station telemetry = Topic(Odometry, endpoint="telem@192.168.1.100:8000") # Network: Commands from ground station commands = Topic(CmdVel, endpoint="cmd@0.0.0.0:8001") # === GROUND STATION (192.168.1.100) === from horus import Topic, CmdVel, Odometry # Receive telemetry from robot telemetry_sub = Topic(Odometry, endpoint="telem@0.0.0.0:8000") # Send commands to robot command_pub = Topic(CmdVel, endpoint="cmd@192.168.1.50:8001") ``` --- ## Integration with Python Ecosystem ### NumPy Integration ```python import horus import numpy as np def process_array(node): if node.has_msg("raw_data"): data = node.recv("raw_data") # Convert to NumPy array arr = np.array(data) # Process with NumPy result = np.fft.fft(arr) node.send("fft_result", result.tolist()) processor = horus.Node( subs="raw_data", pubs="fft_result", tick=process_array ) ``` ### OpenCV Integration ```python import horus import cv2 import numpy as np def process_image(node): if node.has_msg("camera"): img_data = node.recv("camera") # Convert to OpenCV format img = np.array(img_data, dtype=np.uint8).reshape((480, 640, 3)) # Apply OpenCV processing gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150) # Publish result node.send("edges", edges.flatten().tolist()) vision = horus.Node( subs="camera", pubs="edges", tick=process_image, rate=30 ) ``` ### scikit-learn Integration ```python import horus from sklearn.linear_model import LinearRegression import numpy as np model = LinearRegression() def train_model(node): if node.has_msg("training_data"): data = node.recv("training_data") X = np.array(data['features']) y = np.array(data['labels']) # Train model model.fit(X, y) score = model.score(X, y) node.send("model_score", score) trainer = horus.Node( subs="training_data", pubs="model_score", tick=train_model ) ``` --- ## Advanced Patterns ### State Management ```python import horus class RobotState: def __init__(self): self.position = {"x": 0.0, "y": 0.0} self.velocity = 0.0 self.last_update = 0 state = RobotState() def update_state(node): if node.has_msg("velocity"): state.velocity = node.recv("velocity") if node.has_msg("position"): state.position = node.recv("position") # Publish combined state node.send("robot_state", { "pos": state.position, "vel": state.velocity }) state_manager = horus.Node( subs=["velocity", "position"], pubs="robot_state", tick=update_state ) ``` ### Rate Limiting ```python import horus import time class RateLimiter: def __init__(self, min_interval): self.min_interval = min_interval self.last_send = 0 limiter = RateLimiter(min_interval=0.1) # 100ms minimum def rate_limited_publish(node): current_time = time.time() if current_time - limiter.last_send >= limiter.min_interval: node.send("output", "data") limiter.last_send = current_time node = horus.Node( pubs="output", tick=rate_limited_publish, rate=100 # Node runs at 100Hz, but publishes at max 10Hz ) ``` ### Error Handling ```python import horus def safe_processing(node): try: if node.has_msg("input"): data = node.recv("input") result = risky_operation(data) node.send("output", result) except Exception as e: node.send("errors", str(e)) print(f"Error: {e}") processor = horus.Node( subs="input", pubs=["output", "errors"], tick=safe_processing ) ``` --- ## Performance Tips ### 1. Use Per-Node Rate Control ```python # Configure rate and order on the Node, then add to scheduler sensor = horus.Node(name="sensor", tick=sensor_fn, rate=100, order=0) controller = horus.Node(name="ctrl", tick=ctrl_fn, rate=50, order=1) logger = horus.Node(name="logger", tick=log_fn, rate=10, order=2) scheduler = horus.Scheduler() scheduler.add(sensor) scheduler.add(controller) scheduler.add(logger) scheduler.run() # Monitor performance with get_node_stats() stats = scheduler.get_node_stats("sensor") print(f"Sensor executed {stats['total_ticks']} ticks") ``` ### 2. Check Message Freshness ```python import time def control_tick(node): if node.has_msg("sensor_data"): data = node.recv("sensor_data") # Use message-level timestamps for staleness checks if hasattr(data, 'timestamp_ns') and data.timestamp_ns: age_s = (time.time_ns() - data.timestamp_ns) / 1e9 if age_s > 0.1: node.log_warning("Skipping stale sensor data") return process(data) ``` ### 3. Use Dicts for Messages ```python # Send messages as Python dicts (automatically serialized to JSON) cmd = {"linear": 1.5, "angular": 0.8} node.send("cmd_vel", cmd) # For staleness checks, use typed messages with timestamp_ns # or track send time at the application level ``` ### 4. Batch Processing ```python # Use node.recv_all() to process all available messages at once def batch_processor(node): messages = node.recv_all("input") if messages: results = [process(msg) for msg in messages] for result in results: node.send("output", result) ``` ### 5. Keep tick() Fast ```python # GOOD: Fast tick def good_tick(node): if node.has_msg("input"): data = node.recv("input") result = quick_operation(data) node.send("output", result) # BAD: Slow tick def bad_tick(node): time.sleep(1) # Don't block! data = requests.get("http://api.example.com") # Don't do I/O! ``` ### 6. Offload Heavy Processing ```python from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=4) def heavy_processing_node(node): if node.has_msg("input"): data = node.recv("input") # Offload to thread pool future = executor.submit(expensive_operation, data) # Don't block - check result later or use callback ``` ### 7. Use Multiprocess for CPU-Intensive Tasks ```bash # Isolate heavy processing in separate processes horus run sensor.py heavy_vision.py light_controller.py # Each node gets its own CPU core ``` --- ## Development ### Building from Source ```bash # Debug build (fast compile, slow runtime) cd horus_py maturin develop # Release build (slow compile, fast runtime) maturin develop --release # Build wheel for distribution maturin build --release ``` ### Running Tests ```bash # Install test dependencies pip install pytest # Run all tests pytest tests/ # Run specific feature tests horus run tests/test_rate_control.py # Phase 1: Per-node rates horus run tests/test_timestamps.py # Phase 2: Timestamps horus run tests/test_typed_messages.py # Phase 3: Typed messages # With coverage pytest --cov=horus tests/ # Test multiprocess execution (Phase 4) horus run tests/multiprocess_publisher.py tests/multiprocess_subscriber.py ``` ### Mock Mode HORUS Python includes a mock mode for testing without Rust bindings: ```python # If Rust bindings aren't available, automatically falls back to mock # You'll see: "Warning: Rust bindings not available. Running in mock mode." # Use for unit testing Python logic without HORUS running ``` ### Debugging Tips ```python # Check node statistics scheduler = horus.Scheduler() scheduler.add(my_node) # Check node statistics stats = scheduler.get_node_stats("my_node") print(f"Ticks: {stats['total_ticks']}, Errors: {stats['errors_count']}") # Monitor message timestamps via message-level fields msg = node.recv("topic") if msg and hasattr(msg, 'timestamp_ns') and msg.timestamp_ns: age = (time.time_ns() - msg.timestamp_ns) / 1e9 print(f"Message age: {age*1000:.1f}ms") ``` --- ## Interoperability ### With Rust Nodes **Important**: For cross-language communication, use typed topics by passing a message type to `Topic()`. #### Cross-Language with Typed Topics ```python # Python node with typed topic from horus import Topic, CmdVel cmd_topic = Topic(CmdVel) # Typed topic cmd_topic.send(CmdVel(linear=1.0, angular=0.5)) ``` ```rust // Rust node receives use horus::prelude::*; let topic: Topic = Topic::new("cmd_vel")?; if let Some(cmd) = topic.recv() { println!("Got: linear={}, angular={}", cmd.linear, cmd.angular); } ``` #### Generic Topic (String Topics) ```python # Generic Topic - for custom topics from horus import Topic topic = Topic("my_topic") # Pass string for generic topic topic.send({"linear": 1.0, "angular": 0.5}) # Uses JSON serialization ``` **Typed topics:** Use `Topic(CmdVel)`, `Topic(Pose2D)` for cross-language communication. See [Python Message Library](/python/library/python-message-library) for details. --- ## Time API Framework-aware time functions. Use these instead of `time.time()` — they integrate with deterministic mode and SimClock. **Quick reference:** | Function | Returns | Description | |----------|---------|-------------| | `horus.now()` | `float` | Current time in seconds | | `horus.dt()` | `float` | Timestep for this tick in seconds | | `horus.elapsed()` | `float` | Time since scheduler start | | `horus.tick()` | `int` | Current tick number | | `horus.budget_remaining()` | `float` | Time left in tick budget | | `horus.rng_float()` | `float` | Random float in [0, 1) | | `horus.timestamp_ns()` | `int` | Nanosecond timestamp | ### `horus.now() -> float` Current framework time in seconds. - **Normal mode**: Wall clock (`time.time()` equivalent) - **Deterministic mode**: Virtual SimClock that advances by fixed `dt` each tick ```python def tick(node): t = horus.now() node.send("timestamp", t) ``` ### `horus.dt() -> float` Timestep for this tick in seconds. Use this for physics integration instead of measuring elapsed time manually. - **Normal mode**: Actual elapsed time since last tick - **Deterministic mode**: Fixed `1.0 / rate` — identical across runs ```python def tick(node): # PID controller using dt() for correct integration error = target - current integral += error * horus.dt() derivative = (error - prev_error) / horus.dt() output = kp * error + ki * integral + kd * derivative ``` ### `horus.elapsed() -> float` Time elapsed since the scheduler started, in seconds. ```python def tick(node): if horus.elapsed() > 30.0: node.log_info("Running for 30+ seconds, stabilized") ``` ### `horus.tick() -> int` Current tick number (0-indexed, increments each scheduler cycle). ```python def tick(node): if horus.tick() % 100 == 0: node.log_info(f"Tick {horus.tick()}: system healthy") ``` ### `horus.budget_remaining() -> float` Time remaining in this tick's budget, in seconds. Returns `float('inf')` if no budget is configured. Use this for adaptive quality — do more work when time permits, skip expensive operations when tight. ```python def tick(node): # Always do critical work process_sensor_data() # Only do expensive work if budget allows if horus.budget_remaining() > 0.001: # >1ms remaining run_expensive_optimization() ``` ### `horus.rng_float() -> float` Random float in `[0.0, 1.0)`. - **Normal mode**: System entropy (non-deterministic) - **Deterministic mode**: Tick-seeded RNG — produces identical sequences across runs ```python def tick(node): # Simulated sensor noise (reproducible in deterministic mode) noise = (horus.rng_float() - 0.5) * 0.1 reading = true_value + noise ``` ### `horus.timestamp_ns() -> int` Current timestamp in nanoseconds. Use for TransformFrame queries and message timestamping. ```python def tick(node): ts = horus.timestamp_ns() transform = tf.lookup("camera", "base_link", ts) ``` ### Deterministic Mode When using `horus.run(..., deterministic=True)`, the time functions switch from wall clock to SimClock: | Function | Normal Mode | Deterministic Mode | |----------|-------------|-------------------| | `now()` | Wall clock | SimClock (virtual) | | `dt()` | Actual elapsed | Fixed `1/rate` | | `elapsed()` | Real elapsed | Virtual elapsed | | `rng_float()` | System entropy | Tick-seeded (reproducible) | | `tick()` | Same | Same | | `budget_remaining()` | Same | Same | | `timestamp_ns()` | Real nanoseconds | Virtual nanoseconds | This ensures identical behavior across runs — critical for simulation, testing, and replay. --- ## Runtime Parameters `horus.Params` provides dict-like access to runtime configuration stored in `.horus/config/params.yaml`. Change PID gains, speed limits, and thresholds without recompiling. ### `Params(path=None)` Create a parameter store. - `Params()` — loads from `.horus/config/params.yaml` (default) - `Params("path/to/file.yaml")` — loads from explicit path ### Methods | Method | Returns | Description | |--------|---------|-------------| | `get(key, default=None)` | `Any` | Get value, return default if missing | | `params[key]` | `Any` | Get value, raise `KeyError` if missing | | `params[key] = value` | — | Set value | | `has(key)` | `bool` | Check if key exists | | `key in params` | `bool` | Same as `has(key)` | | `keys()` | `List[str]` | All parameter names | | `len(params)` | `int` | Number of parameters | | `save()` | — | Persist to disk | | `remove(key)` | `bool` | Remove a key, returns True if existed | | `reset()` | — | Reset all parameters to defaults | ### Example: Live PID Tuning ```python import horus params = horus.Params() def controller_tick(node): # Read gains from params — change at runtime via CLI or monitor kp = params.get("pid_kp", 1.0) ki = params.get("pid_ki", 0.1) kd = params.get("pid_kd", 0.01) max_speed = params.get("max_speed", 1.5) error = target - current output = min(kp * error, max_speed) node.send("cmd_vel", output) controller = horus.Node(name="PIDController", tick=controller_tick, rate=100, pubs=["cmd_vel"]) horus.run(controller) ``` Set parameters at runtime: ```bash horus param set pid_kp 2.5 horus param set max_speed 0.8 horus param list ``` --- ## Rate Limiter `horus.Rate` provides drift-compensated rate limiting for background threads and standalone loops. For nodes, use the `rate=` constructor kwarg instead. ### `Rate(hz)` Create a rate limiter targeting `hz` iterations per second. ### Methods | Method | Returns | Description | |--------|---------|-------------| | `sleep()` | — | Sleep until next cycle. Compensates for work time to maintain target rate | | `actual_hz()` | `float` | Measured frequency (smoothed average) | | `target_hz()` | `float` | Target frequency | | `period()` | `float` | Target period in seconds (1/hz) | | `is_late()` | `bool` | True if current cycle exceeded the target period | | `reset()` | — | Reset timing (call after a pause to avoid burst catch-up) | ### Example: Camera Capture Thread ```python import threading from horus import Rate, Topic, Image def camera_loop(): rate = Rate(30) # 30 FPS target topic = Topic(Image) while running: frame = capture_camera() topic.send(frame) if rate.is_late(): print(f"Camera behind: {rate.actual_hz():.1f} Hz (target {rate.target_hz():.0f})") rate.sleep() thread = threading.Thread(target=camera_loop, daemon=True) thread.start() ``` --- ## Hardware Drivers `horus.drivers` loads hardware connections from `horus.toml`'s `[drivers]` section. ### Module Functions | Function | Returns | Description | |----------|---------|-------------| | `drivers.load()` | `HardwareSet` | Load drivers from `horus.toml` | | `drivers.load_from(path)` | `HardwareSet` | Load from explicit YAML path | | `drivers.register_driver(name, cls)` | — | Register a Python driver class | ### HardwareSet Returned by `drivers.load()`. Provides typed accessors for each hardware type. | Method | Returns | Description | |--------|---------|-------------| | `hw.list()` | `List[str]` | All driver names | | `hw.has(name)` | `bool` | Check if driver exists | | `name in hw` | `bool` | Same as `has(name)` | | `len(hw)` | `int` | Number of drivers | | `hw.dynamixel(name)` | `DriverParams` | Dynamixel servo bus config | | `hw.serial(name)` | `DriverParams` | Serial port config | | `hw.i2c(name)` | `DriverParams` | I2C bus config | | `hw.can(name)` | `DriverParams` | CAN bus config | | `hw.gpio(name)` | `DriverParams` | GPIO pin config | All typed accessors: `dynamixel`, `rplidar`, `realsense`, `i2c`, `serial`, `can`, `gpio`, `pwm`, `usb`, `webcam`, `input`, `bluetooth`, `net`, `ethercat`, `spi`, `adc`, `raw`. ### DriverParams Dict-like access to a driver's configuration values. | Method | Returns | Description | |--------|---------|-------------| | `params[key]` | `Any` | Get value (KeyError if missing) | | `params.get(key)` | `Any` | Get value (KeyError if missing) | | `params.get_or(key, default)` | `Any` | Get with default if missing | | `params.has(key)` | `bool` | Check if key exists | | `params.keys()` | `List[str]` | All parameter names | ### Example ```toml # horus.toml [drivers] arm = { type = "dynamixel", port = "/dev/ttyUSB0", baudrate = 1000000 } lidar = { type = "rplidar", port = "/dev/ttyUSB1" } ``` ```python import horus hw = horus.drivers.load() if hw.has("arm"): arm = hw.dynamixel("arm") port = arm.get_or("port", "/dev/ttyUSB0") baud = arm.get_or("baudrate", 115200) print(f"Arm on {port} at {baud}") print(f"Available drivers: {hw.list()}") ``` --- ## Unit Constants ```python from horus import us, ms us # 1e-6 — microseconds to seconds ms # 1e-3 — milliseconds to seconds # Use with budget/deadline for readability node = horus.Node(tick=fn, rate=1000, budget=300 * us, deadline=900 * us) ``` --- ## Error Types ```python from horus import HorusNotFoundError, HorusTransformError, HorusTimeoutError try: tf.tf("missing_frame", "base") except HorusTransformError as e: print(f"Transform failed: {e}") try: tf.wait_for_transform("src", "dst", timeout_sec=1.0) except HorusTimeoutError: print("Timed out waiting for transform") ``` | Exception | Rust source | Raised when | |-----------|-------------|-------------| | `HorusNotFoundError` | `NotFound(...)` | Missing topic, frame, node | | `HorusTransformError` | `Transform(...)` | TF extrapolation, stale data | | `HorusTimeoutError` | `Timeout(...)` | Blocking operation timed out | Other Rust errors map to stdlib: `Config` → `ValueError`, `Io` → `IOError`, `Memory` → `MemoryError`, etc. --- ## See Also - [Transform Frame](/python/api/transform-frame) — Coordinate transforms (`TransformFrame`, `Transform`) - [Perception](/python/api/perception) — Detection, landmarks, tracking (`DetectionList`, `PointXYZ`, `COCOPose`) - [Memory Types](/python/api/memory-types) — Image, PointCloud, DepthImage (zero-copy) - [Async Nodes](/python/api/async-nodes) — Async tick functions - [ML Utilities](/python/library/ml-utilities) — PyTorch/ONNX inference helpers --- ## Common Patterns ### Producer-Consumer ```python # Producer producer = horus.Node( pubs="queue", tick=lambda n: n.send("queue", generate_work()) ) # Consumer consumer = horus.Node( subs="queue", tick=lambda n: process_work(n.get("queue")) if n.has_msg("queue") else None ) horus.run(producer, consumer) ``` ### Request-Response ```python def request_node(node): node.send("requests", {"id": 1, "query": "data"}) def response_node(node): if node.has_msg("requests"): req = node.recv("requests") response = handle_request(req) node.send("responses", response) req = horus.Node(pubs="requests", tick=request_node) res = horus.Node(subs="requests", pubs="responses", tick=response_node) ``` ### Periodic Tasks ```python import time class PeriodicTask: def __init__(self, interval): self.interval = interval self.last_run = 0 task = PeriodicTask(interval=5.0) # Every 5 seconds def periodic_tick(node): current = time.time() if current - task.last_run >= task.interval: node.send("periodic", "task_executed") task.last_run = current node = horus.Node(pubs="periodic", tick=periodic_tick, rate=10) ``` --- ## Troubleshooting ### Import Errors ```python # If you see: ModuleNotFoundError: No module named 'horus' # Rebuild and install: cd horus_py maturin develop --release ``` ### Slow Performance ```python # Use release build (not debug) maturin develop --release # Check tick rate isn't too high node = horus.Node(tick=fn, rate=30) # 30Hz is reasonable ``` ### Memory Issues ```python # Avoid accumulating data in closures # BAD: all_data = [] def bad_tick(node): all_data.append(node.recv("input")) # Memory leak! # GOOD: def good_tick(node): data = node.recv("input") process_and_discard(data) # Process immediately ``` --- ## Monitor Integration and Logging ### Current Limitations **Python nodes currently do NOT appear in the HORUS monitor logs.** The Python bindings do not integrate with the Rust logging system: ```python # Python nodes use standard print() for logging print("Debug message") # Visible in console, not in monitor ``` **What this means:** - Python nodes communicate via shared memory - All message passing functionality works - Python log messages don't appear in monitor logs - Use `print()` for Python-side debugging ### Monitoring Python Nodes Since Python nodes don't integrate with the monitor logging system, use these alternatives: 1. **Node-level logging methods:** ```python def tick(node): node.log_info("Processing sensor data") node.log_warning("Sensor reading is stale") node.log_error("Failed to process data") node.log_debug("Debug information") # These print to console, not monitor ``` 2. **Manual topic monitoring:** ```python def tick(node): if node.has_msg("input"): data = node.recv("input") print(f"[{node.name}] Received: {data}") node.send("output", result) print(f"[{node.name}] Published: {result}") ``` 3. **Node statistics:** ```python scheduler = horus.Scheduler() scheduler.add(node) scheduler.run(duration=10) # Get stats after running stats = scheduler.get_node_stats("my_node") print(f"Ticks: {stats['total_ticks']}") print(f"Errors: {stats['errors_count']}") ``` ### Future Improvements Monitor integration for Python nodes is planned for a future release. This will include: - Full `NodeInfo` context in Python callbacks - `LogSummary` for Python message types - Python node logs visible in the monitor TUI and web dashboard --- ### Custom Exceptions HORUS defines three custom exception types plus maps internal errors to standard Python exceptions: ```python from horus import HorusNotFoundError, HorusTransformError, HorusTimeoutError try: result = some_horus_operation() except HorusNotFoundError: print("Resource not found") except HorusTransformError: print("Transform computation failed") except HorusTimeoutError: print("Operation timed out") ``` **Custom exceptions** (inherit from `Exception`): | Exception | When Raised | Rust Source | |-----------|-------------|-------------| | `HorusNotFoundError` | Topic, frame, node, or parent frame not found | `HorusError::NotFound` | | `HorusTransformError` | Transform extrapolation or stale data | `HorusError::Transform` | | `HorusTimeoutError` | Blocking operation exceeded time limit | `HorusError::Timeout` | **Standard Python exceptions** raised by HORUS operations: | Python Exception | When Raised | Rust Source | |------------------|-------------|-------------| | `IOError` | File or IPC I/O failures | `HorusError::Io` | | `MemoryError` | Shared memory or pool allocation failures | `HorusError::Memory` | | `ValueError` | Invalid parameters, bad config, parse errors | `HorusError::InvalidInput`, `InvalidDescriptor`, `Parse`, `Config` | | `TypeError` | Serialization/deserialization failures | `HorusError::Serialization` | | `RuntimeError` | Internal or unmapped errors | All other variants | All exceptions preserve the original Rust error message, so you get full context: ```python try: tf = tf_tree.tf("nonexistent", "world") except HorusNotFoundError as e: print(e) # "Frame not found: nonexistent" try: img = Image(height=-1, width=640, encoding="rgb8") except ValueError as e: print(e) # "Invalid input: height must be positive" ``` **Catch hierarchy** — order matters when catching: ```python try: result = horus_operation() except HorusNotFoundError: pass # Specific: missing resource except HorusTransformError: pass # Specific: TF failure except HorusTimeoutError: pass # Specific: deadline exceeded except (ValueError, TypeError): pass # Bad input or serialization except (IOError, MemoryError): pass # System-level failures except RuntimeError: pass # Catch-all for internal errors ``` --- ## See Also - [Examples](/rust/examples/basic-examples) - More code examples - [Core Concepts](/concepts/core-concepts-nodes) - Understanding HORUS architecture - [Monitor](/development/monitor) - Real-time monitoring and visualization - [Python Message Library](/python/library/python-message-library) - Typed message classes - [Multi-Language Support](/concepts/multi-language) - Cross-language communication - [Performance](/performance/performance) - Optimization guide - [Rust Scheduler API](/rust/api/scheduler) — Rust Scheduler API reference - [Rust Standard Messages](/rust/api/messages) — All Rust POD message types --- **Remember**: With HORUS Python, you focus on *what* your robot does, not *how* the framework works! --- ## Python Message Library Path: /python/library/python-message-library Description: Standard robotics message types for Python — 55+ types with full Rust parity # Python Message Library 55+ typed message classes for Python robotics. Binary-compatible with Rust, zero-copy shared memory transport. **Which messages do I need?** | I'm building a... | Start with these | |---|---| | Mobile robot | `CmdVel`, `Odometry`, `LaserScan`, `Imu` | | Robot arm | `JointState`, `JointCommand`, `WrenchStamped`, `TrajectoryPoint` | | Drone | `Imu`, `NavSatFix`, `MotorCommand`, `BatteryState` | | Vision system | `Image`, `Detection`, `PointCloud`, `DepthImage` | | Multi-robot | `Pose2D`, `Heartbeat`, `DiagnosticStatus`, `TransformStamped` | | Teleoperation | `JoystickInput`, `CmdVel`, `EmergencyStop` | ## Availability All message types are available via the Rust bindings: ```python from horus import ( # Geometry CmdVel, Pose2D, Pose3D, Twist, Vector3, Point3, Quaternion, TransformStamped, PoseStamped, PoseWithCovariance, TwistWithCovariance, Accel, AccelStamped, # Control MotorCommand, ServoCommand, DifferentialDriveCommand, PidConfig, TrajectoryPoint, JointCommand, # Sensor Imu, Odometry, LaserScan, JointState, BatteryState, RangeSensor, NavSatFix, MagneticField, Temperature, FluidPressure, Illuminance, Clock, TimeReference, # Diagnostics Heartbeat, DiagnosticStatus, EmergencyStop, ResourceUsage, DiagnosticValue, DiagnosticReport, NodeHeartbeat, SafetyStatus, # Force/Haptics WrenchStamped, ForceCommand, ContactInfo, ImpedanceParameters, HapticFeedback, # Navigation NavGoal, GoalResult, PathPlan, Waypoint, NavPath, VelocityObstacle, VelocityObstacles, OccupancyGrid, CostMap, # Input JoystickInput, KeyboardInput, # Detection/Perception BoundingBox2D, BoundingBox3D, Detection, Detection3D, SegmentationMask, TrackedObject, TrackingHeader, Landmark, Landmark3D, LandmarkArray, PointField, PlaneDetection, PlaneArray, # Vision CompressedImage, CameraInfo, RegionOfInterest, StereoInfo, # Pool-backed domain types Image, PointCloud, DepthImage, ) ``` ## Overview **Key Features:** - **Full Rust parity** — All 55+ Rust message types are available in Python - **Zero-copy IPC** — POD types transfer via shared memory with no serialization overhead - **Cross-language compatible** — Binary-compatible with Rust message types - **Nanosecond timestamps** — All messages include `timestamp_ns` field - **Typed Topic support** — Use `Topic(CmdVel)` for type-safe pub/sub --- ## Geometry Messages ### Pose2D 2D robot pose (position + orientation). ```python from horus import Pose2D pose = Pose2D(x=1.0, y=2.0, theta=0.5) pose.x = 1.5 pose.y = 2.5 pose.theta = 0.785 ``` **Fields:** `x` (f64), `y` (f64), `theta` (f64), `timestamp_ns` (u64) --- ### Pose3D 3D robot pose (position + quaternion orientation). ```python from horus import Pose3D pose = Pose3D(x=1.0, y=2.0, z=3.0, qx=0.0, qy=0.0, qz=0.0, qw=1.0) ``` **Fields:** `x`, `y`, `z` (f64), `qx`, `qy`, `qz`, `qw` (f64), `timestamp_ns` (u64) --- ### Twist Full 6-DOF velocity (linear + angular) for 3D robots. ```python from horus import Twist twist = Twist(linear_x=1.0, linear_y=0.0, linear_z=0.0, angular_x=0.0, angular_y=0.0, angular_z=0.5) ``` **Fields:** `linear_x`, `linear_y`, `linear_z`, `angular_x`, `angular_y`, `angular_z` (f64), `timestamp_ns` (u64) --- ### Vector3, Point3, Quaternion Basic 3D geometric types. ```python from horus import Vector3, Point3, Quaternion vec = Vector3(x=1.0, y=0.0, z=0.0) point = Point3(x=1.0, y=2.0, z=3.0) quat = Quaternion(x=0.0, y=0.0, z=0.0, w=1.0) ``` --- ### TransformStamped 3D transformation with timestamp. ```python from horus import TransformStamped tf = TransformStamped(tx=1.0, ty=2.0, tz=0.0, rx=0.0, ry=0.0, rz=0.0, rw=1.0) ``` **Fields:** `tx`, `ty`, `tz` (f64 translation), `rx`, `ry`, `rz`, `rw` (f64 rotation quaternion), `timestamp_ns` (u64) --- ### PoseStamped, PoseWithCovariance, TwistWithCovariance Extended pose and velocity types with covariance support. ```python from horus import PoseStamped, PoseWithCovariance, TwistWithCovariance ps = PoseStamped(x=1.0, y=2.0, z=3.0, qx=0.0, qy=0.0, qz=0.0, qw=1.0) pwc = PoseWithCovariance(x=1.0, y=2.0, z=3.0) twc = TwistWithCovariance(linear_x=0.5, angular_z=1.0) ``` --- ### Accel, AccelStamped Linear and angular acceleration. ```python from horus import Accel, AccelStamped accel = Accel(linear_x=9.81, linear_y=0.0, linear_z=0.0, angular_x=0.1, angular_y=0.2, angular_z=0.3) ``` --- ## Control Messages ### CmdVel 2D velocity command (linear + angular). ```python from horus import CmdVel cmd = CmdVel(linear=1.0, angular=0.5) ``` **Fields:** `linear` (f32), `angular` (f32), `timestamp_ns` (u64) --- ### MotorCommand Individual motor control with mode selection. ```python from horus import MotorCommand cmd = MotorCommand(motor_id=0, mode=0, target=1.0, max_velocity=10.0, max_acceleration=5.0, enable=True) ``` **Fields:** `motor_id` (u8), `mode` (u8), `target` (f64), `max_velocity` (f64), `max_acceleration` (f64), `feed_forward` (f64), `enable` (bool), `timestamp_ns` (u64) --- ### ServoCommand Servo motor control. ```python from horus import ServoCommand cmd = ServoCommand(servo_id=0, position=1.57, speed=0.5, enable=True) ``` **Fields:** `servo_id` (u8), `position` (f32), `speed` (f32), `enable` (bool), `timestamp_ns` (u64) --- ### DifferentialDriveCommand, PidConfig ```python from horus import DifferentialDriveCommand, PidConfig diff = DifferentialDriveCommand(left_velocity=1.0, right_velocity=-1.0) pid = PidConfig(kp=1.0, ki=0.1, kd=0.01) ``` --- ### TrajectoryPoint, JointCommand ```python from horus import TrajectoryPoint, JointCommand tp = TrajectoryPoint(position=[1.0, 2.0, 3.0], time_from_start=1.5) ``` --- ## Sensor Messages ### Imu Inertial Measurement Unit data (acceleration + gyroscope). ```python from horus import Imu imu = Imu(accel_x=0.0, accel_y=0.0, accel_z=9.81, gyro_x=0.0, gyro_y=0.0, gyro_z=0.1) ``` **Fields:** `accel_x`, `accel_y`, `accel_z` (f64), `gyro_x`, `gyro_y`, `gyro_z` (f64), `timestamp_ns` (u64) --- ### Odometry Robot odometry (2D pose + velocity). ```python from horus import Odometry odom = Odometry(x=1.0, y=2.0, theta=0.5, linear_velocity=0.5, angular_velocity=0.1) ``` **Fields:** `x`, `y`, `theta` (f64), `linear_velocity`, `angular_velocity` (f64), `timestamp_ns` (u64) --- ### LaserScan 2D LIDAR scan data. ```python from horus import LaserScan scan = LaserScan(angle_min=-3.14, angle_max=3.14, range_min=0.1, range_max=10.0, ranges=[1.0, 1.1, 1.2]) ``` **Fields:** `ranges` (list[f32]), `angle_min`, `angle_max`, `range_min`, `range_max`, `angle_increment` (f32), `timestamp_ns` (u64) --- ### JointState Multi-joint state (up to 16 joints). ```python from horus import JointState js = JointState(names=["j1", "j2"], positions=[1.0, 2.0], velocities=[0.0, 0.0], efforts=[0.0, 0.0]) print(len(js)) # 2 print(js.names) # ["j1", "j2"] print(js.positions) # [1.0, 2.0] ``` --- ### BatteryState, RangeSensor, NavSatFix ```python from horus import BatteryState, RangeSensor, NavSatFix battery = BatteryState(voltage=12.6, percentage=85.0, current=2.5, temperature=25.0) battery.is_low(20.0) # Check if below threshold battery.is_critical() # Check if critical range_s = RangeSensor(range=2.5, min_range=0.02, max_range=10.0, field_of_view=0.44) gps = NavSatFix(latitude=37.7749, longitude=-122.4194, altitude=10.0) gps.has_fix() # Check GPS fix status ``` --- ### MagneticField, Temperature, FluidPressure, Illuminance ```python from horus import MagneticField, Temperature, FluidPressure, Illuminance mag = MagneticField(x=0.25, y=-0.1, z=0.45) temp = Temperature(temperature=72.5, variance=0.1) pressure = FluidPressure(pressure=101325.0, variance=10.0) lux = Illuminance(illuminance=500.0, variance=5.0) ``` --- ### Clock, TimeReference Time synchronization messages. ```python from horus import Clock, TimeReference clk = Clock(clock_ns=1000000000, sim_speed=2.0, source=1) clk.is_paused() # Check pause state tref = TimeReference(time_ref_ns=1000000000, source="gps", offset_ns=-500) corrected = tref.correct_timestamp(local_ns) # Apply offset correction print(tref.source) # "gps" ``` --- ## Diagnostics Messages ```python from horus import (Heartbeat, EmergencyStop, DiagnosticStatus, ResourceUsage, DiagnosticValue, DiagnosticReport, NodeHeartbeat, SafetyStatus) hb = Heartbeat(node_name="controller", node_id=1) estop = EmergencyStop(engaged=True, reason="collision") ds = DiagnosticStatus(level=2, code=101, message="overheating", component="motor") ru = ResourceUsage(cpu_percent=45.5, memory_bytes=1024000) # Structured diagnostic reports dr = DiagnosticReport(component="sensor_hub", level=1) dr.add_value(DiagnosticValue(key="temperature", value="42")) dr.add_value(DiagnosticValue.int("count", 100)) dr.add_value(DiagnosticValue.float("voltage", 24.1)) ss = SafetyStatus() ss.estop_engaged # False (default) ss.watchdog_ok # True (default) ``` --- ## Force/Haptics Messages ```python from horus import (WrenchStamped, ForceCommand, ContactInfo, ImpedanceParameters, HapticFeedback) wrench = WrenchStamped(fx=10.0, fy=0.0, fz=-9.81, tx=0.0, ty=0.5, tz=0.0) force_cmd = ForceCommand(fx=0.0, fy=0.0, fz=-10.0, timeout=1.0) contact = ContactInfo(state=1, contact_force=15.5) impedance = ImpedanceParameters.compliant() # Low stiffness preset impedance = ImpedanceParameters.stiff() # High stiffness preset haptic = HapticFeedback(vibration_intensity=0.8, vibration_frequency=200.0, duration_seconds=0.5) ``` --- ## Navigation Messages ```python from horus import (NavGoal, GoalResult, PathPlan, Waypoint, NavPath, VelocityObstacle, VelocityObstacles, OccupancyGrid, CostMap) goal = NavGoal(x=10.0, y=5.0, theta=0.0, position_tolerance=0.1) result = GoalResult(goal_id=1, status=3, progress=1.0) # Path planning plan = PathPlan(goal_x=10.0, goal_y=5.0) plan.add_waypoint(0.0, 0.0, 0.0) plan.add_waypoint(5.0, 0.0, 0.0) # NavPath with waypoints path = NavPath() path.add_waypoint(Waypoint(x=0.0, y=0.0)) path.add_waypoint(Waypoint(x=5.0, y=5.0, theta=1.57)) print(path.waypoint_count) # 2 # Occupancy grid grid = OccupancyGrid(width=100, height=100, resolution=0.05) grid.set_occupancy(50, 50, 100) # Mark as occupied grid.is_free(2.5, 2.5) # Check if cell is free # Cost map costmap = CostMap(grid=grid, inflation_radius=0.3) ``` --- ## Input Messages ```python from horus import JoystickInput, KeyboardInput joy = JoystickInput(joystick_id=0, element_id=1, value=0.75, pressed=True) key = KeyboardInput(key_name="A", code=65, pressed=True, modifiers=0) ``` --- ## Detection/Perception Messages ```python from horus import (BoundingBox2D, BoundingBox3D, Detection, Detection3D, SegmentationMask, TrackedObject, TrackingHeader, Landmark, Landmark3D, LandmarkArray, PointField, PlaneDetection, PlaneArray) # 2D detection det = Detection(class_name="person", confidence=0.95, x=10.0, y=20.0, width=100.0, height=200.0) # 3D detection det3d = Detection3D(class_name="box", confidence=0.9, cx=1.0, cy=2.0, cz=3.0, length=0.5, width=0.5, height=0.5) # Object tracking tracked = TrackedObject(track_id=42, class_id=1, confidence=0.9, x=1.0, y=2.0, width=3.0, height=4.0) # Landmarks (e.g., body pose keypoints) lm = Landmark(x=100.0, y=200.0, visibility=0.95, index=5) lm3d = Landmark3D(x=1.0, y=2.0, z=3.0, visibility=0.8, index=10) ``` --- ## Vision Messages ```python from horus import (CompressedImage, CameraInfo, RegionOfInterest, StereoInfo, Image, PointCloud, DepthImage) # Compressed image (serde-based) img = CompressedImage(format="jpeg", data=jpeg_bytes, width=640, height=480) # Camera calibration cam = CameraInfo(width=640, height=480, fx=525.0, fy=525.0, cx=320.0, cy=240.0) print(cam.focal_lengths()) # (525.0, 525.0) print(cam.principal_point()) # (320.0, 240.0) # Region of interest roi = RegionOfInterest(x=100, y=200, width=50, height=60) print(roi.area()) # 3000 print(roi.contains(120, 220)) # True # Stereo camera stereo = StereoInfo(left_camera=cam, right_camera=cam, baseline=0.12) # Pool-backed types (zero-copy) image = Image(height=480, width=640, encoding=0) # RGB8 cloud = PointCloud(num_points=1000) depth = DepthImage(height=480, width=640) ``` --- ## Cross-Language Compatibility All 55+ Python message types are **binary-compatible with Rust** via zero-copy shared memory: | Category | Python Classes | Count | |----------|---------------|-------| | **Geometry** | Pose2D, Pose3D, Twist, Vector3, Point3, Quaternion, TransformStamped, PoseStamped, PoseWithCovariance, TwistWithCovariance, Accel, AccelStamped | 12 | | **Control** | CmdVel, MotorCommand, ServoCommand, DifferentialDriveCommand, PidConfig, TrajectoryPoint, JointCommand | 7 | | **Sensor** | Imu, Odometry, LaserScan, JointState, BatteryState, RangeSensor, NavSatFix, MagneticField, Temperature, FluidPressure, Illuminance, Clock, TimeReference | 13 | | **Diagnostics** | Heartbeat, DiagnosticStatus, EmergencyStop, ResourceUsage, DiagnosticValue, DiagnosticReport, NodeHeartbeat, SafetyStatus | 8 | | **Force/Haptics** | WrenchStamped, ForceCommand, ContactInfo, ImpedanceParameters, HapticFeedback | 5 | | **Navigation** | NavGoal, GoalResult, PathPlan, Waypoint, NavPath, VelocityObstacle, VelocityObstacles, OccupancyGrid, CostMap | 9 | | **Input** | JoystickInput, KeyboardInput | 2 | | **Detection/Perception** | BoundingBox2D, BoundingBox3D, Detection, Detection3D, SegmentationMask, TrackedObject, TrackingHeader, Landmark, Landmark3D, LandmarkArray, PointField, PlaneDetection, PlaneArray | 13 | | **Vision** | CompressedImage, CameraInfo, RegionOfInterest, StereoInfo | 4 | | **Domain (pool)** | Image, PointCloud, DepthImage | 3 | **Example — Python to Rust:** ```python # Python sender from horus import Topic, Twist topic = Topic(Twist) topic.send(Twist(linear_x=1.0, angular_z=0.5)) ``` ```rust // Rust receiver use horus::prelude::*; let topic: Topic = Topic::new("twist")?; if let Some(twist) = topic.recv() { println!("linear_x={}, angular_z={}", twist.linear[0], twist.angular[2]); } ``` --- ## Usage Patterns ### Robot Controller with Multiple Sensors ```python from horus import Node, Topic, CmdVel, LaserScan, Imu scan_topic = Topic(LaserScan) imu_topic = Topic(Imu) cmd_topic = Topic(CmdVel) def controller_tick(node): scan = scan_topic.recv() imu = imu_topic.recv() if scan: min_dist = min(scan.ranges) if scan.ranges else None if min_dist and min_dist < 0.5: cmd_topic.send(CmdVel(0.0, 0.0)) # Stop else: cmd_topic.send(CmdVel(linear=0.5, angular=0.0)) node = Node(name="controller", tick=controller_tick, rate=10) ``` --- ## See Also - [Python Bindings](/python/api/python-bindings) — Full Python API guide - [Python Perception Types](/python/api/perception) — DetectionList, TrackedObject, COCOPose - [Custom Messages](/python/api/custom-messages) — Runtime and compiled message generation - [Rust Standard Messages](/rust/api/messages) — Rust API reference for all message types - [Message Types Overview](/concepts/message-types) — Conceptual message type documentation --- ## Custom Messages Path: /python/api/custom-messages Description: Create your own typed messages in Python without writing Rust # Custom Messages (horus.msggen) The `horus.msggen` module lets you define **custom typed messages** directly in Python. Two approaches are available: | Approach | Build Step | Latency | Best For | |----------|-----------|---------|----------| | **Runtime Messages** | None | ~20-40μs | Prototyping, quick iteration | | **Compiled Messages** | `maturin develop` | ~3-5μs | Production, high-frequency | --- ## Runtime Messages (No Build Step) Create custom messages instantly without any compilation. Uses Python's `struct` module for fixed-layout binary serialization. ### Basic Usage ```python from horus.msggen import define_message # Define a custom message type RobotStatus = define_message('RobotStatus', 'robot.status', [ ('battery_level', 'f32'), ('error_code', 'i32'), ('is_active', 'bool'), ('timestamp', 'u64'), ]) # Create instances status = RobotStatus(battery_level=85.0, error_code=0, is_active=True, timestamp=0) # Access fields print(status.battery_level) # 85.0 status.error_code = 5 # Serialize for IPC raw_bytes = status.to_bytes() # 17 bytes # Reconstruct from bytes status2 = RobotStatus.from_bytes(raw_bytes) ``` ### Supported Types | Type String | Size | Description | |-------------|------|-------------| | `f32` / `float32` | 4 bytes | 32-bit float | | `f64` / `float64` | 8 bytes | 64-bit float | | `i8` | 1 byte | Signed 8-bit int | | `i16` | 2 bytes | Signed 16-bit int | | `i32` | 4 bytes | Signed 32-bit int | | `i64` | 8 bytes | Signed 64-bit int | | `u8` | 1 byte | Unsigned 8-bit int | | `u16` | 2 bytes | Unsigned 16-bit int | | `u32` | 4 bytes | Unsigned 32-bit int | | `u64` | 8 bytes | Unsigned 64-bit int | | `bool` | 1 byte | Boolean | ### NumPy Messages (Better Performance) If NumPy is available, use `define_numpy_message` for better performance: ```python from horus.msggen import define_numpy_message import numpy as np # NumPy-based message (uses structured arrays internally) SensorData = define_numpy_message('SensorData', 'sensor.data', [ ('x', np.float32), ('y', np.float32), ('z', np.float32), ('temperature', np.float32), ('timestamp', np.uint64), ]) # Create instance data = SensorData(x=1.0, y=2.0, z=3.0, temperature=25.5, timestamp=0) # Get underlying numpy structured array arr = data.to_numpy() # Zero-copy bytes access raw = data.to_bytes() ``` ### With Topic (IPC) Runtime messages work with Topic for inter-process communication. Use `to_bytes()` / `from_bytes()` for serialization over generic topics: ```python from horus import Topic from horus.msggen import define_message # Define message RobotStatus = define_message('RobotStatus', 'robot.status', [ ('battery_level', 'f32'), ('error_code', 'i32'), ]) # Publisher — use generic topic with manual serialization pub_topic = Topic("robot.status") status = RobotStatus(battery_level=85.0, error_code=0) pub_topic.send(status.to_bytes()) # Subscriber (different process) sub_topic = Topic("robot.status") raw = sub_topic.recv() if raw: received = RobotStatus.from_bytes(raw) print(received.battery_level) # 85.0 ``` Or use the Node convenience API (handles serialization automatically): ```python import horus from horus.msggen import define_message RobotStatus = define_message('RobotStatus', 'robot.status', [ ('battery_level', 'f32'), ('error_code', 'i32'), ]) def publisher_tick(node): status = RobotStatus(battery_level=85.0, error_code=0) node.send("robot.status", status.to_bytes()) def subscriber_tick(node): if node.has_msg("robot.status"): raw = node.recv("robot.status") status = RobotStatus.from_bytes(raw) print(status.battery_level) # 85.0 pub = horus.Node("publisher", pubs="robot.status", tick=publisher_tick) sub = horus.Node("subscriber", subs="robot.status", tick=subscriber_tick) horus.run(pub, sub, duration=3) ``` --- ## Compiled Messages (Production) For maximum performance (~3-5μs), compile your messages to Rust. This generates PyO3 bindings with the same zero-copy performance as built-in types. ### Step 1: Define Messages ```python from horus.msggen import register_message # Register one or more messages register_message('RobotStatus', 'robot.status', [ ('battery_level', 'f32'), ('error_code', 'i32'), ('is_active', 'bool'), ('timestamp', 'u64'), ]) register_message('SensorReading', 'sensor.reading', [ ('x', 'f64'), ('y', 'f64'), ('z', 'f64'), ]) ``` ### Step 2: Build ```python from horus.msggen import build_messages # Generate Rust code and rebuild build_messages() # Runs: maturin develop --release ``` This generates Rust code in `horus_py/src/custom_messages/` and rebuilds the module. ### Step 3: Use After building, your messages are available directly from `horus`: ```python from horus import RobotStatus, SensorReading, Topic # Create typed topic topic = Topic(RobotStatus) # Send status = RobotStatus(battery_level=85.0, error_code=0, is_active=True, timestamp=0) topic.send(status) # Receive (typed!) received = topic.recv() print(received.battery_level) ``` ### YAML Schema (Recommended for Teams) For larger projects, define messages in YAML: ```yaml # messages.yaml messages: - name: RobotStatus topic: robot.status fields: - name: battery_level type: f32 - name: error_code type: i32 - name: is_active type: bool - name: SensorReading topic: sensor.reading fields: - name: x type: f64 - name: y type: f64 - name: z type: f64 ``` ```python from horus.msggen import generate_messages_from_yaml, build_messages generate_messages_from_yaml('messages.yaml') build_messages() ``` ### Rebuild Detection The builder tracks message definitions via hash. It won't rebuild unless messages change: ```python from horus.msggen import check_needs_rebuild, build_messages if check_needs_rebuild(): build_messages() else: print("Messages are up to date") ``` Force rebuild with: ```python build_messages(force=True) ``` --- ## Performance Comparison | Approach | Latency | Throughput | Use Case | |----------|---------|------------|----------| | **Built-in (Rust)** | ~3μs | 300K msgs/sec | CmdVel, Pose2D, etc. | | **Compiled Custom** | ~3-5μs | 200K msgs/sec | Production custom types | | **Runtime** | ~20-40μs | 25K msgs/sec | Prototyping | | **Runtime (NumPy)** | ~15-30μs | 35K msgs/sec | NumPy integration | | **Pickle** | ~50-100μs | 10K msgs/sec | Legacy/dynamic types | **Recommendation**: Start with runtime messages for fast iteration, then compile for production. --- ## API Reference ### define_message ```python def define_message( name: str, topic: str, fields: List[Tuple[str, str]] ) -> Type[RuntimeMessage] ``` Create a runtime message class. **Parameters:** - `name`: Class name (e.g., `"RobotStatus"`) - `topic`: Topic name (e.g., `"robot.status"`) - `fields`: List of `(field_name, type_string)` tuples **Returns:** New message class ### define_numpy_message ```python def define_numpy_message( name: str, topic: str, fields: List[Tuple[str, Any]] ) -> Type[NumpyMessage] ``` Create a NumPy-based message class. **Parameters:** - `name`: Class name - `topic`: Topic name - `fields`: List of `(field_name, numpy_dtype)` tuples **Returns:** New NumPy message class ### register_message ```python def register_message( name: str, topic: str, fields: List[Tuple[str, str]] ) -> None ``` Register a message for compiled generation. ### build_messages ```python def build_messages( force: bool = False, verbose: bool = True ) -> bool ``` Build all registered messages. **Parameters:** - `force`: Rebuild even if unchanged - `verbose`: Print progress **Returns:** `True` if successful ### check_needs_rebuild ```python def check_needs_rebuild() -> bool ``` Check if registered messages differ from last build. --- ## Complete Example ```python #!/usr/bin/env python3 """Custom message example with runtime messages.""" import horus from horus.msggen import define_message # Define custom sensor message MySensor = define_message('MySensor', 'my.sensor', [ ('distance', 'f32'), ('angle', 'f32'), ('confidence', 'f32'), ('object_id', 'u32'), ]) def sensor_tick(node): """Publish sensor readings.""" reading = MySensor( distance=2.5, angle=0.785, confidence=0.95, object_id=42 ) node.send("my.sensor", reading.to_bytes()) def processor_tick(node): """Process sensor readings.""" if node.has_msg("my.sensor"): raw = node.recv("my.sensor") reading = MySensor.from_bytes(raw) print(f"Object {reading.object_id}: {reading.distance}m at {reading.angle}rad") # Create nodes sensor = horus.Node("sensor", pubs="my.sensor", tick=sensor_tick, rate=10) processor = horus.Node("processor", subs="my.sensor", tick=processor_tick) # Run horus.run(sensor, processor, duration=3) ``` --- ## When to Use Each Approach ### Use Runtime Messages When: - Prototyping new message types - Message schema changes frequently - You don't want to wait for compilation - Performance requirements are moderate (<50Hz) ### Use Compiled Messages When: - Deploying to production - High-frequency data (>100Hz) - Cross-language compatibility required - Type safety is critical ### Use Built-in Messages When: - Standard robotics types (CmdVel, Pose2D, LaserScan) - Maximum performance needed - Compatibility with other HORUS systems --- ## Python Memory Types Path: /python/api/memory-types Description: Zero-copy Image, PointCloud, and DepthImage with NumPy/PyTorch/JAX interop # Python Memory Types Pool-backed types for images, point clouds, and tensors. Zero-copy interop with NumPy, PyTorch, and JAX. **Quick example** — camera processing pipeline with numpy: ```python import horus from horus import Image, Topic import numpy as np sub_rgb = Topic(Image, "camera.rgb") pub_edges = Topic(Image, "camera.edges") def edge_tick(node): img = sub_rgb.try_recv() if img is not None: # Zero-copy to numpy — no pixel data copied pixels = img.to_numpy() # Process with numpy (Sobel edge detection) gray = np.mean(pixels, axis=2).astype(np.uint8) edges = np.abs(np.diff(gray, axis=1)) # Zero-copy back to HORUS Image result = Image.from_numpy(edges) pub_edges.send(result) detector = horus.Node(name="edge_detector", tick=edge_tick, rate=30, subs=["camera.rgb"], pubs=["camera.edges"]) ``` ## Image Pool-backed camera image with zero-copy framework conversions. ### Creating Images ```python from horus import Image # Create empty RGB image (height, width, encoding) img = Image(height=480, width=640, encoding="rgb8") # From NumPy array (zero-copy when possible) import numpy as np pixels = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) img = Image.from_numpy(pixels, encoding="rgb8") # From PyTorch tensor import torch tensor = torch.zeros(480, 640, 3, dtype=torch.uint8) img = Image.from_torch(tensor, encoding="rgb8") # From raw bytes img = Image.from_bytes(raw_data, height=480, width=640, encoding="rgb8") ``` ### Supported Encodings | Encoding | Channels | Bytes/Pixel | Description | |----------|----------|-------------|-------------| | `"mono8"` | 1 | 1 | 8-bit grayscale | | `"mono16"` | 1 | 2 | 16-bit grayscale | | `"rgb8"` | 3 | 3 | 8-bit RGB | | `"bgr8"` | 3 | 3 | 8-bit BGR (OpenCV) | | `"rgba8"` | 4 | 4 | 8-bit RGBA | | `"bgra8"` | 4 | 4 | 8-bit BGRA | | `"yuv422"` | 2 | 2 | YUV 4:2:2 | | `"mono32f"` | 1 | 4 | 32-bit float mono | | `"rgb32f"` | 3 | 12 | 32-bit float RGB | | `"bayer_rggb8"` | 1 | 1 | Bayer raw | | `"depth16"` | 1 | 2 | 16-bit depth (mm) | ### Properties ```python img.height # Image height in pixels img.width # Image width in pixels img.channels # Number of channels (e.g., 3 for RGB) img.encoding # Encoding string (e.g., "rgb8") img.dtype # Data type string img.nbytes # Total data size in bytes img.step # Row stride in bytes img.frame_id # Sensor frame identifier img.timestamp_ns # Timestamp in nanoseconds ``` ### Framework Conversions (Zero-Copy) ```python # To NumPy — zero-copy, shared memory np_array = img.to_numpy() # Shape: (H, W, C) for color, (H, W) for mono # To PyTorch — zero-copy via DLPack torch_tensor = img.to_torch() # To JAX — zero-copy via DLPack jax_array = img.to_jax() ``` ### Pixel Access ```python # Read pixel at (x, y) pixel = img.pixel(320, 240) # Returns list, e.g., [128, 64, 255] # Write pixel img.set_pixel(320, 240, [255, 0, 0]) # Red pixel # Fill entire image with a color img.fill([0, 0, 0]) # Black # Copy data from bytes img.copy_from(raw_bytes) # Extract region of interest (raw bytes) roi_data = img.roi(x=100, y=100, w=200, h=200) ``` ### Metadata ```python img.set_frame_id("camera_front") img.set_timestamp_ns(1234567890) # Device info img.is_cpu() # True (always CPU-backed currently) ``` ### DLPack Protocol Image implements the DLPack protocol for framework-agnostic zero-copy: ```python # NumPy array protocol np_array = np.asarray(img) # Uses __array_interface__ # DLPack (PyTorch, JAX, CuPy, etc.) capsule = img.__dlpack__() device = img.__dlpack_device__() ``` --- ## PointCloud Pool-backed 3D point cloud with zero-copy ML framework interop. ### Creating Point Clouds ```python from horus import PointCloud # Create XYZ point cloud (num_points, fields_per_point, dtype) cloud = PointCloud(num_points=10000, fields=3, dtype="float32") # From NumPy array — shape (N, F) where F = fields per point import numpy as np points = np.random.randn(10000, 3).astype(np.float32) cloud = PointCloud.from_numpy(points) # From PyTorch tensor import torch tensor = torch.randn(10000, 3) cloud = PointCloud.from_torch(tensor) ``` ### Properties ```python cloud.point_count # Number of points cloud.fields_per_point # Floats per point (3=XYZ, 4=XYZI, 6=XYZRGB) cloud.dtype # Data type string cloud.nbytes # Total data size in bytes cloud.frame_id # Sensor frame identifier cloud.timestamp_ns # Timestamp in nanoseconds # Point format queries cloud.is_xyz() # True if 3 fields (XYZ) cloud.has_intensity() # True if 4+ fields (XYZI) cloud.has_color() # True if 6+ fields (XYZRGB) ``` ### Framework Conversions ```python # To NumPy — shape (N, F), zero-copy np_points = cloud.to_numpy() # To PyTorch — zero-copy via DLPack torch_points = cloud.to_torch() # To JAX — zero-copy via DLPack jax_points = cloud.to_jax() ``` ### Point Access ```python # Get i-th point as list of floats (float32 clouds only) point = cloud.point_at(0) # e.g., [1.0, 2.0, 3.0] ``` ### Metadata and DLPack ```python cloud.set_frame_id("lidar_front") cloud.set_timestamp_ns(1234567890) cloud.is_cpu() # True (always CPU-backed currently) # DLPack protocol capsule = cloud.__dlpack__() ``` --- ## DepthImage Pool-backed depth image supporting F32 (meters) and U16 (millimeters) formats. ### Creating Depth Images ```python from horus import DepthImage # Create F32 depth image (meters) depth = DepthImage(height=480, width=640, dtype="float32") # Create U16 depth image (millimeters) depth_u16 = DepthImage(height=480, width=640, dtype="uint16") # From NumPy — shape (H, W) import numpy as np depth_data = np.random.uniform(0.5, 5.0, (480, 640)).astype(np.float32) depth = DepthImage.from_numpy(depth_data) # From PyTorch import torch depth = DepthImage.from_torch(torch.randn(480, 640)) ``` ### Properties ```python depth.height # Image height depth.width # Image width depth.dtype # "float32" or "uint16" depth.nbytes # Total data size depth.frame_id # Camera frame identifier depth.timestamp_ns # Timestamp depth.depth_scale # Scale factor depth.is_meters() # True if F32 (meters) depth.is_millimeters() # True if U16 (millimeters) ``` ### Depth Access ```python # Get depth at pixel (always returns meters as float) d = depth.get_depth(320, 240) print(f"Depth at center: {d:.3f}m") # Set depth at pixel (value in meters) depth.set_depth(100, 100, 1.5) # Get statistics (min, max, mean) — None if no valid data stats = depth.depth_statistics() if stats: min_d, max_d, mean_d = stats print(f"Range: {min_d:.2f}-{max_d:.2f}m, mean: {mean_d:.2f}m") ``` ### Framework Conversions ```python np_depth = depth.to_numpy() # Shape: (H, W) torch_depth = depth.to_torch() jax_depth = depth.to_jax() ``` --- ## Memory Management Image, PointCloud, and DepthImage are backed by a shared memory pool that handles allocation, reference counting, and cross-process transport automatically. You don't need to manage the pool directly — HORUS creates and sizes it for you. For custom tensor shapes that don't fit Image/PointCloud/DepthImage, use `GenericMessage` or define a custom typed message with the `message!` macro. --- ## Usage with Topics All memory types work seamlessly with typed topics for zero-copy IPC: ```python from horus import Topic, Image, PointCloud, DepthImage import numpy as np # Publish an image img_topic = Topic(Image) img = Image.from_numpy(np.zeros((480, 640, 3), dtype=np.uint8), encoding="rgb8") img_topic.send(img) # Receive an image received = img_topic.recv() if received: np_img = received.to_numpy() # Zero-copy access print(f"Received {received.width}x{received.height} image") ``` ## ML Pipeline Example ```python import horus from horus import Image, Topic import numpy as np img_topic = Topic(Image) def camera_tick(node): frame = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) img = Image.from_numpy(frame, encoding="rgb8") img.set_frame_id("camera_front") img_topic.send(img) def inference_tick(node): img = img_topic.recv() if img: # Zero-copy to PyTorch for inference tensor = img.to_torch() # No data copy! # ... run model ... camera = horus.Node(name="camera", tick=camera_tick, rate=30, order=0) model = horus.Node(name="model", tick=inference_tick, rate=30, order=1) horus.run(camera, model) ``` ## See Also - [Python Bindings](/python/api/python-bindings) — Core Python API - [ML Utilities](/python/library/ml-utilities) — ML framework integration - [Image API (Rust)](/rust/api/image) — Rust Image reference - [PointCloud API (Rust)](/rust/api/pointcloud) — Rust PointCloud reference - [DepthImage API (Rust)](/rust/api/depth-image) — Rust DepthImage reference --- ## Python TransformFrame Path: /python/api/transform-frame Description: Coordinate frame management and 3D transform lookups in Python # Python TransformFrame HORUS provides a Python API for coordinate frame management — registering frames, updating transforms, and looking up transformations between any two frames in the tree. ## Transform A 3D rigid transformation (translation + quaternion rotation). ### Creating Transforms ```python from horus import Transform # Identity transform tf = Transform.identity() # From translation and rotation quaternion tf = Transform(translation=[1.0, 2.0, 0.0], rotation=[0.0, 0.0, 0.0, 1.0]) # From translation only (identity rotation) tf = Transform.from_translation([1.0, 2.0, 3.0]) # From Euler angles (translation + roll/pitch/yaw) tf = Transform.from_euler([1.0, 0.0, 0.5], [0.0, 0.1, 1.57]) # From 4x4 homogeneous matrix tf = Transform.from_matrix(matrix_4x4) ``` ### Properties ```python tf.translation # [x, y, z] as list of floats tf.rotation # [x, y, z, w] quaternion as list of floats ``` Both are readable and writable: ```python tf.translation = [2.0, 3.0, 0.0] tf.rotation = [0.0, 0.0, 0.383, 0.924] # 45 degrees around Z ``` ### Methods ```python # Convert to Euler angles roll, pitch, yaw = tf.to_euler() # Compose transforms: result = self * other combined = parent_tf.compose(child_tf) # Inverse transform inv = tf.inverse() # Apply to a point point_in_world = tf.transform_point([1.0, 0.0, 0.0]) # Apply rotation only (no translation) rotated = tf.transform_vector([1.0, 0.0, 0.0]) # Interpolate between two transforms (SLERP for rotation) halfway = tf_a.interpolate(tf_b, t=0.5) # Magnitudes dist = tf.translation_magnitude() # Translation distance angle = tf.rotation_angle() # Rotation angle in radians # Export as 4x4 matrix matrix = tf.to_matrix() # [[f64; 4]; 4] ``` | Method | Returns | Description | |--------|---------|-------------| | `identity()` | `Transform` | No translation, no rotation | | `from_translation([x,y,z])` | `Transform` | Translation only | | `from_euler([x,y,z], [r,p,y])` | `Transform` | Translation + Euler angles | | `from_matrix(4x4)` | `Transform` | From homogeneous matrix | | `to_euler()` | `[roll, pitch, yaw]` | Get Euler angles | | `compose(other)` | `Transform` | Chain transforms (self * other) | | `inverse()` | `Transform` | Compute inverse | | `transform_point([x,y,z])` | `[x,y,z]` | Apply full transform to point | | `transform_vector([x,y,z])` | `[x,y,z]` | Apply rotation only to vector | | `interpolate(other, t)` | `Transform` | SLERP interpolation (0.0-1.0) | | `translation_magnitude()` | `float` | Translation distance | | `rotation_angle()` | `float` | Rotation angle in radians | | `to_matrix()` | `4x4 list` | Export as homogeneous matrix | --- ## TransformFrame The frame tree manager. Stores a hierarchy of coordinate frames and computes transforms between any two frames. ### Creating a TransformFrame ```python from horus import TransformFrame, TransformFrameConfig # Default configuration tf_tree = TransformFrame() # With custom config config = TransformFrameConfig(max_frames=1024, history_len=64) tf_tree = TransformFrame(config=config) # Preset sizes tf_tree = TransformFrame.small() # 256 frames, ~550KB tf_tree = TransformFrame.medium() # 1024 frames, ~2.2MB tf_tree = TransformFrame.large() # 4096 frames, ~9MB tf_tree = TransformFrame.massive() # 16384 frames, ~35MB ``` ### Registering Frames ```python # Register child frame under a parent — returns frame ID (int) frame_id = tf_tree.register_frame("base_link", "world") tf_tree.register_frame("lidar", "base_link") tf_tree.register_frame("camera", "base_link") # Unregister a frame (raises HorusNotFoundError if not found) tf_tree.unregister_frame("camera") ``` ### Updating Transforms ```python from horus import Transform # Set the transform from parent to child (raises on unknown frame) tf_tree.update_transform("base_link", Transform( translation=[1.0, 0.0, 0.0], rotation=[0.0, 0.0, 0.0, 1.0] )) # With optional explicit timestamp tf_tree.update_transform("lidar", Transform.from_translation([0.0, 0.0, 0.3]), timestamp_ns=1234567890) ``` ### Looking Up Transforms ```python # Get transform from source frame to target frame tf = tf_tree.tf("lidar", "world") print(f"Lidar in world: {tf.translation}") # With specific timestamp (for interpolation) tf = tf_tree.tf_at("lidar", "world", timestamp_ns=1234567890) ``` ### Querying the Tree ```python # List all registered frames frames = tf_tree.all_frames() # ["world", "base_link", "lidar", "camera"] # Get parent of a frame parent = tf_tree.parent("lidar") # "base_link" # Get children of a frame children = tf_tree.children("base_link") # ["lidar", "camera"] # Seconds since last transform update (None if never updated) age = tf_tree.time_since_last_update("lidar") # e.g., 0.015 # Wait for a transform to become available (blocking with timeout) tf = tf_tree.wait_for_transform("lidar", "world", timeout_sec=1.0) print(f"Got transform: {tf.translation}") ``` ### Frame Registration | Method | Returns | Description | |--------|---------|-------------| | `register_frame(name, parent)` | `int` | Register dynamic frame, returns frame ID | | `register_static_frame(name, transform, parent=None)` | `int` | Register frame with fixed transform | | `unregister_frame(name)` | `None` | Remove a frame | | `has_frame(name)` | `bool` | Check if frame exists | | `frame_count()` | `int` | Number of registered frames | | `frame_id(name)` | `int` | Get numeric frame ID from name | | `frame_name(id)` | `str` | Get frame name from numeric ID | ### Updating Transforms | Method | Returns | Description | |--------|---------|-------------| | `update_transform(name, tf, timestamp_ns=None)` | `None` | Set transform for a frame | | `update_transform_by_id(frame_id, tf, timestamp_ns=None)` | `None` | Update by numeric ID (faster) | | `set_static_transform(name, transform)` | `None` | Set a static (never-changing) transform | ### Looking Up Transforms | Method | Returns | Description | |--------|---------|-------------| | `tf(source, target)` | `Transform` | Latest transform between frames | | `tf_at(source, target, ts)` | `Transform` | Transform at specific timestamp (interpolated) | | `tf_at_strict(source, target, ts)` | `Transform` | Exact timestamp match (no interpolation) | | `tf_at_with_tolerance(src, dst, ts, tolerance_ns=100_000_000)` | `Transform` | Interpolated with tolerance window | | `tf_by_id(src_id, dst_id)` | `Transform` | Look up by numeric IDs (fastest) | | `can_transform(source, target)` | `bool` | Check if transform path exists | | `can_transform_at(src, dst, ts)` | `bool` | Check if available at timestamp | | `can_transform_at_with_tolerance(src, dst, ts, tolerance_ns)` | `bool` | Check with tolerance | | `wait_for_transform(src, dst, timeout_sec)` | `Transform` | Block until available | | `wait_for_transform_async(src, dst, timeout_sec)` | `Future[Transform]` | Async wait (returns `concurrent.futures.Future`) | ### Applying Transforms | Method | Returns | Description | |--------|---------|-------------| | `transform_point(source, target, [x,y,z])` | `[x,y,z]` | Transform a point between frames | | `transform_vector(source, target, [x,y,z])` | `[x,y,z]` | Transform a vector (rotation only) | ### Querying the Tree | Method | Returns | Description | |--------|---------|-------------| | `all_frames()` | `list[str]` | All registered frame names | | `parent(name)` | `str` or `None` | Parent frame name | | `children(name)` | `list[str]` | Child frame names | | `frame_chain(source, target)` | `list[str]` | Frame path from source to target | | `time_since_last_update(name)` | `float` or `None` | Seconds since last update | | `is_stale(name, max_age_sec=1.0)` | `bool` | Check if frame data is stale | ### Diagnostics | Method | Returns | Description | |--------|---------|-------------| | `stats()` | `dict` | Frame tree statistics | | `validate()` | `bool` | Validate tree integrity | | `frame_info(name)` | `dict` | Metadata for a single frame | | `frame_info_all()` | `list[dict]` | Metadata for all frames | | `format_tree()` | `str` | Human-readable tree visualization | | `frames_as_dot()` | `str` | DOT graph format (for Graphviz) | | `frames_as_yaml()` | `str` | YAML export of frame tree | #### `stats()` Return Value The `stats()` method returns a dictionary with the following keys: | Key | Type | Description | |-----|------|-------------| | `total_frames` | `int` | Total registered frames | | `static_frames` | `int` | Frames that never change | | `dynamic_frames` | `int` | Frames updated at runtime | | `max_frames` | `int` | Maximum capacity | | `history_len` | `int` | Transform history buffer size | | `tree_depth` | `int` | Maximum depth of the frame tree | | `root_count` | `int` | Number of root frames (no parent) | ```python stats = tf_tree.stats() print(f"Frames: {stats['total_frames']}/{stats['max_frames']}") print(f"Tree depth: {stats['tree_depth']}, Roots: {stats['root_count']}") ``` #### `frame_info(name)` Return Value The `frame_info(name)` method returns a dictionary with metadata for a single frame: | Key | Type | Description | |-----|------|-------------| | `name` | `str` | Frame name | | `id` | `int` | Internal frame ID | | `parent` | `str` or `None` | Parent frame name (`None` for root) | | `is_static` | `bool` | Whether this frame never changes | ```python info = tf_tree.frame_info("camera") print(f"Frame: {info['name']}, Parent: {info['parent']}, Static: {info['is_static']}") ``` ### Advanced Usage ```python # Static frames — set once, never changes tf_tree.register_static_frame("lidar", Transform.from_translation([0.0, 0.0, 0.3]), parent="base_link") # Transform points directly between frames world_point = tf_tree.transform_point("lidar", "world", [5.0, 0.0, 0.0]) # Async wait (non-blocking) import concurrent.futures future = tf_tree.wait_for_transform_async("lidar", "world", timeout_sec=2.0) tf = future.result() # Blocks until ready # Check staleness before using data if tf_tree.is_stale("base_link", max_age_sec=0.1): print("Odometry data is stale!") # Diagnostics print(tf_tree.format_tree()) # Visual tree print(tf_tree.stats()) # {"frames": 5, "lookups": 1234, ...} tf_tree.validate() # Checks tree integrity ``` --- ## TransformFrameConfig Configuration for the frame tree. ```python from horus import TransformFrameConfig # Custom config = TransformFrameConfig(max_frames=512, history_len=16) # Presets config = TransformFrameConfig.small() # 256 frames, ~550KB config = TransformFrameConfig.medium() # 1024 frames, ~2.2MB config = TransformFrameConfig.large() # 4096 frames, ~9MB config = TransformFrameConfig.massive() # 16384 frames, ~35MB # Check memory usage print(config.max_frames) # 512 print(config.history_len) # 16 print(config.memory_estimate()) # "~1.1MB" ``` --- ## Complete Example ```python from horus import Node, Scheduler, TransformFrame, Transform import math # Global transform tree tf_tree = TransformFrame.medium() def setup_frames(node): tf_tree.register_frame("base_link", "world") tf_tree.register_frame("lidar", "base_link") tf_tree.register_frame("camera", "base_link") # Static transform: lidar is 30cm above base tf_tree.update_transform("lidar", Transform.from_translation([0.0, 0.0, 0.3])) # Static transform: camera is 10cm forward, 15cm up tf_tree.update_transform("camera", Transform.from_translation([0.1, 0.0, 0.15])) tick_count = 0 def odometry_tick(node): global tick_count tick_count += 1 # Simulate robot moving in a circle t = tick_count * 0.01 x = math.cos(t) * 2.0 y = math.sin(t) * 2.0 yaw = t + math.pi / 2 # Update base_link in world tf_tree.update_transform("base_link", Transform.from_euler([x, y, 0.0], [0.0, 0.0, yaw])) def perception_tick(node): # Transform a lidar point into world coordinates try: lidar_to_world = tf_tree.tf("lidar", "world") point_in_world = lidar_to_world.transform_point([5.0, 0.0, 0.0]) node.log_info(f"Obstacle at world: {point_in_world}") except Exception: pass # Frame not yet available odom = Node(name="odom", tick=odometry_tick, init=setup_frames, rate=100, order=0) percept = Node(name="perception", tick=perception_tick, rate=10, order=1) scheduler = Scheduler() scheduler.add(odom) scheduler.add(percept) scheduler.run(duration=10) ``` ## Utility ```python from horus import get_timestamp_ns # Get current time in nanoseconds (same clock as Rust) now = get_timestamp_ns() ``` ## See Also - [TransformFrame Concepts](/concepts/transform-frame) — Architecture and design - [Python Bindings](/python/api/python-bindings) — Core Python API - [Geometry Messages](/rust/api/geometry-messages) — TransformStamped type --- ## Perception Types Path: /python/api/perception Description: Python perception types for object detection, tracking, pose estimation, and point cloud processing # Python Perception Types Types for computer vision pipelines — object detection, tracking, pose estimation, and point cloud processing. **Quick example** — publish YOLO detection results: ```python import horus from horus import Image, Detection, DetectionList, BoundingBox2D, Topic model = load_yolo("model.pt") sub_image = Topic(Image, "camera.rgb") pub_detections = Topic(DetectionList, "detections") def detector_tick(node): img = sub_image.try_recv() if img is not None: results = model.predict(img.to_numpy()) detections = DetectionList() for r in results: det = Detection( label=r.class_name, confidence=r.score, bbox=BoundingBox2D( x=r.x, y=r.y, width=r.w, height=r.h, ), ) detections.add(det) pub_detections.send(detections) detector = horus.Node(name="detector", tick=detector_tick, rate=30, subs=["camera.rgb"], pubs=["detections"]) ``` ```python from horus import ( BoundingBox2D, Detection, DetectionList, PointXYZ, PointXYZRGB, PointCloudBuffer, Landmark, Landmark3D, LandmarkArray, TrackedObject, COCOPose, ) ``` --- ## BoundingBox2D 2D bounding box in pixel coordinates. ```python bbox = BoundingBox2D(x=10.0, y=20.0, width=100.0, height=200.0) bbox = BoundingBox2D.from_center(cx=60.0, cy=120.0, width=100.0, height=200.0) ``` | Property / Method | Returns | Description | |-------------------|---------|-------------| | `.x`, `.y`, `.width`, `.height` | `float` | Box coordinates | | `.center_x()`, `.center_y()` | `float` | Center point | | `.area()` | `float` | Area in pixels² | | `.iou(other)` | `float` | Intersection over Union | | `.as_tuple()` | `(x, y, w, h)` | XYWH format | | `.as_xyxy()` | `(x1, y1, x2, y2)` | Corner format | --- ## Detection 2D object detection result. ```python det = Detection(class_name="person", confidence=0.95, x=10.0, y=20.0, width=100.0, height=200.0) det = Detection.from_bbox(bbox, class_name="car", confidence=0.87) ``` | Property / Method | Returns | Description | |-------------------|---------|-------------| | `.bbox` | `BoundingBox2D` | Bounding box | | `.confidence` | `float` | Detection confidence (0-1) | | `.class_id` | `int` | Numeric class identifier | | `.class_name` | `str` | Class label string | | `.instance_id` | `int` | Instance tracking ID | | `.is_confident(threshold)` | `bool` | Check if above threshold | | `.to_bytes()` / `.from_bytes(data)` | `bytes` / `Detection` | Serialization | --- ## DetectionList Filterable collection of detections with iteration support. ```python detections = DetectionList() detections.append(Detection("person", 0.95, 10, 20, 100, 200)) detections.append(Detection("car", 0.72, 300, 150, 80, 60)) detections.append(Detection("person", 0.45, 500, 100, 90, 180)) # Filter by confidence confident = detections.filter_confidence(0.7) # 2 detections # Filter by class people = detections.filter_class("person") # 2 detections # Iterate for det in detections: print(f"{det.class_name}: {det.confidence:.2f}") # Index access first = detections[0] count = len(detections) # Convert to dicts (for JSON/logging) dicts = detections.to_dicts() # Serialization data = detections.to_bytes() restored = DetectionList.from_bytes(data) ``` | Method | Returns | Description | |--------|---------|-------------| | `.append(det)` | — | Add a detection | | `.filter_confidence(threshold)` | `DetectionList` | Keep detections above threshold | | `.filter_class(name)` | `DetectionList` | Keep only matching class | | `.to_dicts()` | `list[dict]` | Convert to list of Python dicts | | `.to_bytes()` / `.from_bytes(data)` | `bytes` / `DetectionList` | Serialization | | `len(detections)` | `int` | Number of detections | | `detections[i]` | `Detection` | Index access | | `for det in detections` | — | Iteration | --- ## PointXYZ / PointXYZRGB Individual 3D point types. ```python point = PointXYZ(x=1.0, y=2.0, z=3.0) print(point.distance()) # Distance from origin print(point.distance_to(other_point)) # Distance between points np_arr = point.to_numpy() # [1.0, 2.0, 3.0] colored = PointXYZRGB(x=1.0, y=2.0, z=3.0, r=255, g=0, b=0) print(colored.rgb()) # (255, 0, 0) print(colored.xyz()) # PointXYZ(1.0, 2.0, 3.0) ``` --- ## PointCloudBuffer Mutable point cloud buffer for building point clouds incrementally. ```python buffer = PointCloudBuffer(capacity=10000, frame_id="lidar_front") # Add points one at a time buffer.add_point(1.0, 2.0, 3.0) buffer.add_point(4.0, 5.0, 6.0) # From NumPy — shape (N, 3) buffer = PointCloudBuffer.from_numpy(np_points, frame_id="lidar") # Access point = buffer[0] # PointXYZ count = len(buffer) np_arr = buffer.to_numpy() # Shape (N, 3) data = buffer.to_bytes() # Serialization ``` --- ## TrackedObject Object with tracking state (for multi-object tracking pipelines). ```python tracked = TrackedObject( track_id=42, bbox=BoundingBox2D(10, 20, 100, 200), class_name="person", confidence=0.95, ) ``` | Property / Method | Returns | Description | |-------------------|---------|-------------| | `.track_id` | `int` | Unique track identifier | | `.bbox` | `BoundingBox2D` | Current bounding box | | `.confidence` | `float` | Detection confidence | | `.class_id` / `.class_name` | `int` / `str` | Class info | | `.velocity` | `(float, float)` | Estimated (vx, vy) in pixels/frame | | `.speed()` | `float` | Speed magnitude | | `.age` | `int` | Frames since creation | | `.hits` | `int` | Successful detections | | `.is_tentative()` | `bool` | Not yet confirmed | | `.is_confirmed()` | `bool` | Track is confirmed | | `.is_deleted()` | `bool` | Track marked for deletion | | `.update(bbox, confidence)` | — | Update with new detection | | `.mark_missed()` | — | No detection this frame | | `.confirm()` / `.delete()` | — | State transitions | ### Tracking Pipeline Example ```python from horus import DetectionList, TrackedObject, Topic tracks: dict[int, TrackedObject] = {} def tracker_tick(node): detections = det_topic.recv() if not detections: return # Simple nearest-neighbor matching matched = match_detections(tracks, detections) for track_id, det in matched.items(): tracks[track_id].update(det.bbox, det.confidence) for track_id in unmatched_tracks: tracks[track_id].mark_missed() if tracks[track_id].age > 30: tracks[track_id].delete() ``` --- ## COCOPose Constants for COCO 17-keypoint pose estimation. ```python from horus import COCOPose # Keypoint indices COCOPose.NOSE # 0 COCOPose.LEFT_EYE # 1 COCOPose.RIGHT_EYE # 2 COCOPose.LEFT_EAR # 3 COCOPose.RIGHT_EAR # 4 COCOPose.LEFT_SHOULDER # 5 COCOPose.RIGHT_SHOULDER # 6 COCOPose.LEFT_ELBOW # 7 COCOPose.RIGHT_ELBOW # 8 COCOPose.LEFT_WRIST # 9 COCOPose.RIGHT_WRIST # 10 COCOPose.LEFT_HIP # 11 COCOPose.RIGHT_HIP # 12 COCOPose.LEFT_KNEE # 13 COCOPose.RIGHT_KNEE # 14 COCOPose.LEFT_ANKLE # 15 COCOPose.RIGHT_ANKLE # 16 COCOPose.NUM_KEYPOINTS # 17 ``` ### Pose Estimation Example ```python from horus import Landmark, LandmarkArray, COCOPose # Create landmarks from pose model output landmarks = LandmarkArray(num_landmarks=17, dimension=2) landmarks.confidence = 0.92 # Access specific keypoints nose = Landmark(x=320.0, y=200.0, visibility=0.99, index=COCOPose.NOSE) left_wrist = Landmark(x=450.0, y=380.0, visibility=0.85, index=COCOPose.LEFT_WRIST) if nose.is_visible(0.5) and left_wrist.is_visible(0.5): dist = nose.distance_to(left_wrist) print(f"Nose to wrist: {dist:.1f}px") ``` --- ## See Also - [Python Message Library](/python/library/python-message-library) — All 55+ message types - [Memory Types](/python/api/memory-types) — Image, PointCloud, DepthImage - [ML Utilities](/python/library/ml-utilities) — ML framework integration - [Rust Perception Messages](/rust/api/perception-messages) — Rust PointCloud, Landmark, TrackedObject types - [Rust Standard Messages](/rust/api/messages) — All Rust POD message types --- ## Python API Path: /python/api Description: HORUS Python bindings API reference # Python API Complete Python API documentation for HORUS. The Python bindings are built with PyO3, exposing the core Rust framework to Python with zero-copy shared memory IPC. ## [Python Bindings](/python/api/python-bindings) Full reference for the HORUS Python bindings: - `Node` — Create nodes with `tick`, `init`, and `shutdown` callbacks - `Topic` — Unified pub/sub with typed messages (CmdVel, Pose2D, Imu, etc.) - `Scheduler` — Node execution with priority ordering, rate control, and composable builder methods - `TransformFrame` / `Transform` — Coordinate frame management - `Image` / `PointCloud` / `DepthImage` — Zero-copy shared memory data with DLPack ## [Custom Messages](/python/api/custom-messages) Create your own typed messages in Python: - **Runtime messages** - No build step, ~20-40μs latency - **Compiled messages** - Requires maturin, ~3-5μs latency - NumPy-based messages for better performance - YAML schema support for team projects ## [Async Nodes](/python/api/async-nodes) Asynchronous Python nodes for non-blocking I/O operations. ## [Memory Types](/python/api/memory-types) Zero-copy Image, PointCloud, and DepthImage with NumPy/PyTorch/JAX interop. ## [Perception Types](/python/api/perception) Object detection, tracking, pose estimation — DetectionList, TrackedObject, COCOPose, PointCloudBuffer. ## [TransformFrame](/python/api/transform-frame) Coordinate frame management — register frames, look up transforms, interpolate, export. --- ## Quick Reference ### Core Classes | Class | Description | |-------|-------------| | `Node` | Computation unit — all config via kwargs (`tick`, `rate`, `order`, `budget`, etc.) | | `Topic` | Standalone pub/sub channel for inter-process IPC | | `Scheduler` | Node execution orchestrator (composable kwargs, no presets) | | `Params` | Dict-like runtime parameter store from `horus.toml` | | `Rate` | Drift-compensated rate limiter for background threads | | `NodeState` | Node lifecycle states | ### Message Types | Class | Fields | Default Topic | |-------|--------|---------------| | `CmdVel` | `linear`, `angular` | `"cmd_vel"` | | `Pose2D` | `x`, `y`, `theta` | `"pose"` | | `Imu` | `accel_x/y/z`, `gyro_x/y/z` | `"imu"` | | `Odometry` | `x`, `y`, `theta`, `linear_velocity`, `angular_velocity` | `"odom"` | | `LaserScan` | `ranges`, `angle_min/max`, `range_min/max` | `"scan"` | ### Transform System | Class | Description | |-------|-------------| | `TransformFrame` | Coordinate frame tree with transform lookups | | `Transform` | 3D transformation (translation + quaternion rotation) | | `TransformFrameConfig` | TransformFrame configuration with size presets | ### Perception Types Available in `horus.perception`: | Class | Description | |-------|-------------| | `Detection` | Object detection result (bbox, class, confidence) | | `DetectionList` | Collection of detections with filtering | | `BoundingBox2D` | 2D bounding box with IoU calculation | | `PointCloudBuffer` | Point cloud with NumPy integration | | `TrackedObject` | Object tracking with velocity estimation | ### Large Data Types | Class | Description | |-------|-------------| | `Image` | Pool-backed camera image with zero-copy framework conversions | | `PointCloud` | Pool-backed 3D point cloud with zero-copy ML interop | | `DepthImage` | Pool-backed depth image (F32 meters or U16 millimeters) | ### Networking | Class | Description | |-------|-------------| | `RouterClient` | Connect to HORUS network router | | `RouterServer` | Host a HORUS network router | --- ## Installation ```bash pip install horus-robotics ``` ## Minimal Example ```python import horus def my_tick(node): node.send("greeting", "Hello from Python!") msg = node.recv("greeting") if msg: print(msg) node = horus.Node( name="MyNode", pubs=["greeting"], subs=["greeting"], tick=my_tick, rate=10 ) horus.run(node) ``` ======================================== # SECTION: Python Guide ======================================== --- ## Nodes & Topics Path: /python-guide/nodes-topics Description: Create Python nodes, publish and subscribe to topics, and communicate with Rust nodes via shared memory # Nodes & Topics in Python Python nodes run at full shared-memory speed (~500ns latency) and can communicate seamlessly with Rust nodes on the same topics. ## Creating a Node ### Functional Style ```python import horus from horus import CmdVel def drive_tick(node): node.send("cmd_vel", CmdVel(linear=0.5, angular=0.0)) def drive_shutdown(node): print("Drive node stopped") drive = horus.Node( name="drive_node", tick=drive_tick, shutdown=drive_shutdown, pubs=["cmd_vel"], rate=50, order=0 ) horus.run(drive) ``` Optional callbacks: | Callback | Called | Required | |----------|--------|----------| | `init(node)` | Once at startup | No | | `tick(node)` | Every cycle | Yes | | `shutdown(node)` | Once at exit | No | ### Minimal Style For simple nodes, skip the callbacks entirely: ```python import horus def publish_temp(node): node.send("temperature", 25.5) sensor = horus.Node( name="temp_sensor", pubs="temperature", tick=publish_temp, rate=10 ) horus.run(sensor, duration=30) ``` ## Creating Topics ```python from horus import Topic, CmdVel, LaserScan, Image # Typed topic (validates message types) cmd = Topic("cmd_vel", CmdVel) # Untyped topic (accepts any serializable data) data = Topic("raw_data") # Large data types use zero-copy automatically camera = Topic("camera/rgb", Image) ``` The topic name is the connection key. Any node (Python or Rust) publishing to `"cmd_vel"` connects to any subscriber on `"cmd_vel"`. ## Sending Messages ```python from horus import Topic, CmdVel cmd = Topic("cmd_vel", CmdVel) # Send a typed message cmd.send(CmdVel(linear=1.0, angular=0.5)) # With functional nodes, use node.send() def my_tick(node): node.send("cmd_vel", {"linear": 1.0, "angular": 0.0}) ``` ## Receiving Messages ```python from horus import Topic, LaserScan scan = Topic("scan", LaserScan) # Get next message (returns None if empty) msg = scan.recv() if msg is not None: print(f"Min range: {min(msg.ranges)}") ``` With functional nodes, use `node.recv()`: ```python def process(node): if node.has_msg("scan"): scan = node.recv("scan") # One message all_scans = node.recv_all("scan") # All pending messages ``` ### Timestamps Timestamps are managed by the Rust Topic backend. Messages that include a timestamp field (e.g., `msg.timestamp_ns`) can be used for latency and staleness checks at the application level: ```python def monitor(node): if node.has_msg("sensor"): msg = node.recv("sensor") # Use message-level timestamps for timing if hasattr(msg, 'timestamp_ns') and msg.timestamp_ns: import time age_s = (time.time_ns() - msg.timestamp_ns) / 1e9 if age_s > 0.5: node.log_warning("Sensor data is stale!") ``` ## Type-Safe Messages Use HORUS built-in types for compile-time safety and cross-language compatibility: ```python from horus import CmdVel, Imu, Pose2D, LaserScan, Image # Built-in types match Rust types exactly cmd = CmdVel(linear=1.0, angular=0.5) pose = Pose2D(x=1.0, y=2.0, theta=0.5) ``` ### Custom Messages Define your own message types with `horus.msggen`: ```python from horus.msggen import define_message MotorStatus = define_message('MotorStatus', 'motor.status', [ ('motor_id', 'u32'), ('velocity', 'f32'), ('temperature', 'f32'), ]) status = MotorStatus(motor_id=1, velocity=3.14, temperature=45.0) topic = Topic("motor.status") topic.send(status) ``` ## Mixed Rust + Python Communication Rust and Python nodes share the same shared memory. A topic name connects them automatically — no path configuration needed. **Rust publisher:** ```rust use horus::prelude::*; let topic = Topic::::new("cmd_vel")?; topic.send(CmdVel { linear: 1.0, angular: 0.0 }); ``` **Python subscriber:** ```python from horus import Topic, CmdVel cmd = Topic("cmd_vel", CmdVel) msg = cmd.recv() if msg is not None: print(f"linear={msg.linear}, angular={msg.angular}") ``` Run them together: ```bash horus run motor_control.rs planner.py logger.py ``` All processes communicate through shared memory with sub-microsecond latency. ## Complete Example: Sensor Pipeline ```python import horus from horus import LaserScan, CmdVel, Topic scan_topic = Topic("scan", LaserScan) cmd_topic = Topic("cmd_vel", CmdVel) def avoider_tick(node): scan = scan_topic.recv() if scan is not None: min_range = min(r for r in scan.ranges if scan.range_min < r < scan.range_max) if min_range < 0.5: cmd_topic.send(CmdVel(linear=0.0, angular=0.5)) else: cmd_topic.send(CmdVel(linear=1.0, angular=0.0)) avoider = horus.Node(name="obstacle_avoider", tick=avoider_tick, rate=30, order=0, subs=["scan"], pubs=["cmd_vel"]) horus.run(avoider) ``` ## See Also - [Python Bindings Reference](/python/api/python-bindings) — Full API reference - [Custom Messages](/python/api/custom-messages) — Runtime and compiled message types - [Async Nodes](/python/api/async-nodes) — Async patterns for network and I/O - [Multi-Language Systems](/concepts/multi-language) — Mixing Rust and Python nodes --- ## Python Guide Path: /python-guide Description: Build robotics and ML applications in Python with HORUS — rapid prototyping, sensor processing, neural network inference # Python Guide Python is ideal for **ML pipelines, rapid prototyping, and sensor processing** with HORUS. All Python nodes communicate with Rust nodes via zero-copy shared memory. ## Quick Start ```python import horus from horus import CmdVel def my_tick(node): node.send("cmd_vel", CmdVel(linear=0.5, angular=0.0)) my_node = horus.Node(name="my_node", tick=my_tick, rate=50, pubs=["cmd_vel"]) horus.run(my_node) ``` ## When to Use Python vs Rust | Use Python for | Use Rust for | |---------------|-------------| | ML inference (PyTorch, ONNX) | Motor control loops (1kHz+) | | Computer vision pipelines | Safety-critical nodes | | Rapid prototyping | Low-latency sensor processing | | Data analysis and logging | Production deployment | | Research experiments | Hard real-time requirements | ## Guide Contents 1. **[Nodes & Topics](/python-guide/nodes-topics)** — Creating nodes, pub/sub, mixed Rust+Python communication 2. **[Python Bindings](/python/api/python-bindings)** — Full Node, Topic, Scheduler API in Python 3. **[Async Nodes](/python/api/async-nodes)** — Async patterns for network and I/O 3. **[ML Integration](/development/ai-integration)** — PyTorch, ONNX, DLPack tensor exchange 4. **[Custom Messages](/python/api/custom-messages)** — Define your own message types 5. **[Memory Types](/python/api/memory-types)** — Image, PointCloud, Tensor (zero-copy) 6. **[ML Utilities](/python/library/ml-utilities)** — TensorPool, DLPack helpers 7. **[Examples](/python/examples)** — Working Python applications ======================================== # SECTION: Development ======================================== --- ## Logging Path: /development/logging Description: Structured node logging with hlog!, hlog_once!, and hlog_every! macros # Logging HORUS provides structured, node-aware logging macros that write to both the console and a shared memory buffer (visible in `horus monitor` and `horus log`). ```rust use horus::prelude::*; ``` --- ## hlog! — Standard Logging Log a message with a level and the current node context: ```rust hlog!(info, "Sensor initialized on port {}", port); hlog!(warn, "Battery at {}% — consider charging", pct); hlog!(error, "Failed to read IMU: {}", err); hlog!(debug, "Raw accelerometer: {:?}", accel); ``` ### Log Levels | Level | Color | Use For | |-------|-------|---------| | `info` | Blue | Normal operation events (startup, config loaded, calibration done) | | `warn` | Yellow | Abnormal but recoverable conditions (battery low, sensor noisy) | | `error` | Red | Failures that need attention (hardware disconnected, topic timeout) | | `debug` | Gray | Detailed information for development (raw values, timing) | ### Output Format Logs appear on stderr with color and node attribution: ```text [INFO] [SensorNode] Initialized on /dev/ttyUSB0 [WARN] [BatteryMonitor] Battery at 15% — consider charging [ERROR] [MotorController] Failed to read encoder: timeout [DEBUG] [Planner] Path computed in 2.3ms, 47 waypoints ``` The scheduler automatically sets the node context before each `tick()`, `init()`, and `shutdown()` call — you don't need to pass the node name manually. ### Example in a Node ```rust use horus::prelude::*; struct SensorNode { port: String, readings: u64, } impl Node for SensorNode { fn name(&self) -> &str { "SensorNode" } fn init(&mut self) { hlog!(info, "Starting sensor on {}", self.port); } fn tick(&mut self) { self.readings += 1; hlog!(debug, "Reading #{}", self.readings); if self.readings % 1000 == 0 { hlog!(info, "Processed {} readings", self.readings); } } fn shutdown(&mut self) { hlog!(info, "Sensor shutting down after {} readings", self.readings); } } ``` --- ## hlog_once! — Log Once Per Run Log a message exactly once, regardless of how many times the callsite executes. Subsequent calls from the same location are silently ignored. ```rust fn tick(&mut self) { if let Some(frame) = self.camera.recv() { hlog_once!(info, "First frame received: {}x{}", frame.width, frame.height); // Only logs on the very first frame — silent after that } } ``` Equivalent to ROS2's `RCLCPP_INFO_ONCE`. **Use for:** - First-time events ("calibration complete", "first message received") - One-time warnings ("running without GPU acceleration") - Init messages inside `tick()` that only matter once --- ## hlog_every! — Rate-Limited Logging Log at most once per N milliseconds. Prevents log flooding from high-frequency nodes. ```rust fn tick(&mut self) { // At most once per second, even if tick() runs at 1000 Hz hlog_every!(1000, info, "Position: ({:.2}, {:.2})", self.x, self.y); // At most once per 5 seconds hlog_every!(5000, warn, "Battery: {}%", self.battery_pct); // At most once per 200ms (5 Hz logging from a 100 Hz node) hlog_every!(200, debug, "Encoder ticks: L={} R={}", self.left, self.right); } ``` **Syntax:** `hlog_every!(interval_ms, level, format, args...)` Equivalent to ROS2's `RCLCPP_INFO_THROTTLE`. **Use for:** - Status updates from high-frequency nodes (>10 Hz) - Periodic health reports - Any log inside `tick()` that would otherwise flood the console --- ## Viewing Logs ### Console Logs appear on stderr in real-time with color coding. ### `horus log` CLI ```bash # View all logs horus log # Follow live (like tail -f) horus log -f # Filter by node name horus log SensorNode # Filter by level horus log --level warn # Show last N entries horus log -n 50 # Filter by time horus log -s "5m ago" # Clear logs horus log --clear ``` ### `horus monitor` The web dashboard (`horus monitor`) and TUI (`horus monitor -t`) show a live log stream with filtering by node and level. --- ## Python Logging Python nodes use method calls instead of macros: ```python import horus def sensor_tick(node): node.log_info(f"Reading: {value}") node.log_warning("Battery low") node.log_error("Sensor disconnected") node.log_debug(f"Raw: {raw_data}") sensor = horus.Node(name="SensorNode", tick=sensor_tick, rate=10) ``` --- ## Best Practices | Do | Don't | |----|-------| | `hlog_every!(1000, ...)` in `tick()` at >10 Hz | `hlog!(info, ...)` every tick at 1000 Hz | | `hlog_once!(info, "First frame")` for one-time events | `if self.first { hlog!(...); self.first = false; }` | | `hlog!(info, ...)` in `init()` and `shutdown()` | Log only in `tick()` | | Include units: `"speed: {:.1} m/s"` | Bare numbers: `"speed: {}"` | | Use `debug` for high-volume data | Use `info` for everything | --- ## See Also - [Monitor](/development/monitor) — live log dashboard - [CLI Reference](/development/cli-reference) — `horus log` command details - [BlackBox](/advanced/blackbox) — flight recorder for post-mortem analysis --- ## Telemetry Export Path: /development/telemetry Description: Export runtime metrics to HTTP, UDP, file, or stdout for external monitoring and dashboards # Telemetry Export HORUS can export runtime metrics (node tick durations, message counts, deadline misses, etc.) to external monitoring systems. Telemetry runs on the scheduler's export cycle — never blocks the real-time loop. ```rust use horus::prelude::*; ``` --- ## Quick Start Enable telemetry with a single builder method: ```rust let mut scheduler = Scheduler::new() .tick_rate(100_u64.hz()) .telemetry("http://localhost:9090/metrics"); // HTTP POST endpoint scheduler.add(MyNode::new()?).order(0).rate(100_u64.hz()).build()?; scheduler.run()?; ``` The scheduler exports a JSON snapshot every 1 second (default interval). --- ## Endpoint Types The `.telemetry(endpoint)` method accepts a string that determines the export backend: | String Format | Endpoint | Behavior | |--------------|----------|----------| | `"http://host:port/path"` | HTTP POST | Non-blocking — background thread handles network I/O | | `"https://host:port/path"` | HTTPS POST | Same as HTTP with TLS | | `"udp://host:port"` | UDP datagram | Compact JSON, single packet per snapshot | | `"file:///path/to/metrics.json"` | Local file | Pretty-printed JSON, overwritten each export | | `"/path/to/metrics.json"` | Local file | Same as `file://` prefix | | `"stdout"` or `"local"` | Stdout | Pretty-printed to terminal (debugging) | | `"disabled"` or `""` | Disabled | No export (default) | ### HTTP Endpoint (Recommended for Production) ```rust let mut scheduler = Scheduler::new() .telemetry("http://localhost:9090/metrics"); ``` HTTP export is **fully non-blocking** for the scheduler: 1. Scheduler calls `export()` on its cycle — posts a snapshot to a bounded channel (capacity 4) 2. A dedicated background thread reads from the channel and performs the HTTP POST 3. If the channel is full (receiver slow), the snapshot is silently dropped — the scheduler never blocks ### UDP Endpoint (Low Overhead) ```rust let mut scheduler = Scheduler::new() .telemetry("udp://192.168.1.100:9999"); ``` Sends compact single-line JSON per snapshot. Good for LAN monitoring where packet loss is acceptable. ### File Endpoint (Debugging & Logging) ```rust let mut scheduler = Scheduler::new() .telemetry("/tmp/horus-metrics.json"); ``` Overwrites the file on each export cycle. Useful for debugging or feeding into log aggregation pipelines. --- ## JSON Payload Format Every export produces a `TelemetrySnapshot`: ```json { "timestamp_secs": 1710547200, "scheduler_name": "motor_control", "uptime_secs": 42.5, "metrics": [ { "name": "node.tick_duration_us", "value": { "Gauge": 145.2 }, "labels": { "node": "MotorCtrl" }, "timestamp_secs": 1710547200 }, { "name": "node.total_ticks", "value": { "Counter": 4250 }, "labels": { "node": "MotorCtrl" }, "timestamp_secs": 1710547200 }, { "name": "scheduler.deadline_misses", "value": { "Counter": 0 }, "labels": {}, "timestamp_secs": 1710547200 } ] } ``` ### Metric Value Types | Type | JSON | Description | |------|------|-------------| | Counter | `{ "Counter": 42 }` | Monotonically increasing (total ticks, messages sent) | | Gauge | `{ "Gauge": 3.14 }` | Current value (tick duration, CPU usage) | | Histogram | `{ "Histogram": [0.1, 0.2, 0.15] }` | Distribution of values | | Text | `{ "Text": "Healthy" }` | String status | --- ## Auto-Collected Metrics When telemetry is enabled, the scheduler automatically exports: | Metric Name | Type | Labels | Description | |-------------|------|--------|-------------| | `node.total_ticks` | Counter | `node` | Total ticks executed | | `node.tick_duration_us` | Gauge | `node` | Last tick duration in microseconds | | `node.errors` | Counter | `node` | Total tick errors | | `scheduler.deadline_misses` | Counter | — | Total deadline misses across all nodes | | `scheduler.uptime_secs` | Gauge | — | Scheduler uptime | --- ## Feature Flag Telemetry requires the `telemetry` feature on `horus_core` — **enabled by default**. To disable at compile time (saves binary size): ```toml [dependencies] horus = { version = "0.1", default-features = false, features = ["macros", "blackbox"] } ``` --- ## Integration with External Tools ### Grafana + Custom Receiver Write a small HTTP server that receives the JSON POST and forwards metrics to Prometheus/InfluxDB: ```python # receiver.py — minimal Flask example from flask import Flask, request app = Flask(__name__) @app.route("/metrics", methods=["POST"]) def metrics(): snapshot = request.json for m in snapshot["metrics"]: print(f"{m['name']} = {m['value']}") return "ok", 200 app.run(port=9090) ``` ### horus monitor (Alternative) For local debugging, `horus monitor` provides a built-in TUI dashboard — no external setup needed. See [Monitor](/development/monitor) for details. --- ## Complete Example ```rust use horus::prelude::*; struct SensorNode { pub_topic: Topic, } impl SensorNode { fn new() -> Result { Ok(Self { pub_topic: Topic::new("imu.raw")? }) } } impl Node for SensorNode { fn name(&self) -> &str { "Sensor" } fn tick(&mut self) { self.pub_topic.send(Imu { orientation: [1.0, 0.0, 0.0, 0.0], angular_velocity: [0.0, 0.0, 0.0], linear_acceleration: [0.0, 0.0, 9.81], }); } } fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(100_u64.hz()) .telemetry("http://localhost:9090/metrics"); // export every 1s scheduler.add(SensorNode::new()?) .order(0) .rate(100_u64.hz()) .build()?; scheduler.run() } ``` --- ## Monitor Guide Path: /development/monitor Description: Monitor, debug, and manage your HORUS applications in real-time # Monitor > **Under Development**: The HORUS Monitor is under active development. Core monitoring features work (nodes, topics, graph, parameters, packages). Some functionality (remote deployment, recordings browser) is still being finalized. The HORUS Monitor provides a real-time view of your running robot system through a web interface or terminal UI. ## Quick Start ```bash # Start your HORUS application horus run # In another terminal, start the monitor horus monitor ``` Browser opens automatically to `http://localhost:3000`. On first run, you'll be prompted to set a password (or press Enter to skip). ```bash # Custom port horus monitor 8080 # Terminal UI mode (no browser needed) horus monitor --tui # Reset password horus monitor --reset-password ``` ## Web Interface The web monitor has **3 main tabs**: ### Monitor Tab The main monitoring view with two sub-views: **List View** — Shows nodes and topics in a grid layout: - **Nodes card**: All running nodes with their status - **Topics card**: Active message channels with sizes **Graph View** — Interactive canvas showing: - Nodes as circles connected to their topics - Visual representation of the pub/sub network - Helps answer "which nodes are talking to which topics?" A **status bar** at the top always shows: - Active Nodes count (hover for node list) - Active Topics count (hover for topic list) - Monitor port ### Parameters Tab Live runtime parameter editor: - **Search** parameters by name - **Add** new parameters at runtime - **Edit** existing values (changes apply immediately) - **Delete** parameters - **Export** all parameters to file - **Import** parameters from file Useful for tuning PID gains, speed limits, sensor thresholds without restarting. ### Packages Tab Browse and manage HORUS packages: - Search the registry - Install packages - Manage environments ## Terminal UI Mode For SSH sessions and headless servers: ```bash horus monitor --tui ``` The TUI provides **8 tabs** navigated with arrow keys: | Tab | Description | |-----|-------------| | **Overview** | System health summary with log panel | | **Nodes** | Running nodes with detailed metrics | | **Topics** | Active topics and message flow | | **Network** | Network connections and transport status | | **TransformFrame** | TransformFrame protocol inspection | | **Packages** | Package management | | **Params** | Runtime parameter editor | | **Recordings** | Session recordings browser | ### Topic Debug Logging In the **Topics** tab, press **Enter** on any topic to enable runtime debug logging. All `send()` and `recv()` calls on that topic will emit live log entries showing direction, IPC latency, and message summaries (if `LogSummary` is implemented). Press **Esc** to disable logging — zero overhead resumes immediately. No code changes or recompilation required. ## Network Access The monitor binds to all network interfaces (`0.0.0.0`), so you can access it from: - Same machine: `http://localhost:3000` - Any device on the network: `http://:3000` **Always set a password** when the monitor is network-accessible. ## Security The monitor supports password-based authentication for networked deployments. ### Setup On first run, set a password (or press Enter to skip authentication): ```bash horus monitor [SECURITY] HORUS Monitor - First Time Setup Password: ******** Confirm password: ******** [SUCCESS] Password set successfully! ``` Reset password anytime: ```bash horus monitor --reset-password ``` ### How Authentication Works When a password is set: 1. The web UI shows a login page before granting access 2. All API endpoints require a valid session token (except `/api/login`) 3. Sessions expire after 1 hour of inactivity 4. Failed login attempts are rate-limited When no password is set (Enter pressed at setup): - All endpoints are accessible without authentication - Suitable for local development only ### API Authentication ```bash # Login — returns a session token curl -X POST http://localhost:3000/api/login \ -H "Content-Type: application/json" \ -d '{"password": "your_password"}' # Returns: {"token": "abc123..."} # Use token for API requests curl http://localhost:3000/api/nodes \ -H "Authorization: Bearer abc123..." # Logout curl -X POST http://localhost:3000/api/logout \ -H "Authorization: Bearer abc123..." ``` ### Security Details | Feature | Value | |---------|-------| | Password hashing | Argon2id | | Session timeout | 1 hour inactivity | | Rate limiting | 5 attempts per 60 seconds | | Token size | 256-bit random (base64-encoded) | Password hash stored at `~/.horus/dashboard_password.hash`. For production deployments, consider placing a reverse proxy with TLS (e.g., nginx) in front of the monitor. ### Recovery If locked out: ```bash # Option 1: Reset via CLI horus monitor --reset-password # Option 2: Delete the password hash file rm ~/.horus/dashboard_password.hash horus monitor # Re-prompts for password setup ``` ## API Endpoints The monitor exposes a REST API (authenticated when a password is set): | Endpoint | Method | Description | |----------|--------|-------------| | `/api/status` | GET | System health status | | `/api/nodes` | GET | Running nodes info | | `/api/topics` | GET | Active topics | | `/api/graph` | GET | Node-topic graph | | `/api/network` | GET | Network connections | | `/api/logs/all` | GET | All logs | | `/api/logs/node/:name` | GET | Logs for specific node | | `/api/logs/topic/:name` | GET | Logs for specific topic | | `/api/params` | GET | List parameters | | `/api/params/:key` | GET/POST/DELETE | Get/set/delete parameter | | `/api/params/export` | POST | Export all parameters | | `/api/params/import` | POST | Import parameters | | `/api/packages/registry` | GET | Search packages | | `/api/packages/install` | POST | Install package | | `/api/packages/uninstall` | POST | Uninstall package | | `/api/recordings` | GET | List recordings | | `/api/login` | POST | Authenticate | | `/api/logout` | POST | End session | ## Common Scenarios ### Debugging Message Flow **"My subscriber isn't getting messages"** 1. Open Monitor tab, switch to Graph View 2. Is there an arrow from publisher -> topic -> subscriber? 3. If not: check topic name matches or node isn't running **"The robot is running slow"** 1. Check nodes list for high CPU usage 2. Check tick rates — which node can't keep up? 3. Use logs endpoint to check for slow tick warnings ### Live Parameter Tuning **Tuning PID controller:** 1. Open Parameters tab 2. Search for `pid` 3. Edit `pid.kp` value — change applies instantly 4. Watch robot behavior, adjust until optimal 5. Export final values with Export button ## Troubleshooting **Monitor shows nothing** Make sure your HORUS application is running first (`horus run`). **Can't access from another device** Check both devices are on the same network. Allow port through firewall: `sudo ufw allow 3000`. **Port already in use** Specify a different port: `horus monitor 8080`. **Password reset** Run `horus monitor -r` or delete `~/.horus/dashboard_password.hash`. ## Next Steps - **[CLI Reference](/development/cli-reference)** - Full CLI command reference - **[Parameters Guide](/development/parameters)** - Deep dive into runtime parameters --- ## Testing HORUS Applications Path: /development/testing Description: Unit testing, integration testing, and mocking for HORUS nodes # Testing HORUS Applications Learn how to test your HORUS nodes and applications with complete, runnable examples using Rust's built-in test framework. ## Why Test HORUS Nodes? Testing ensures: - **Nodes work in isolation** before integration - **Message passing is correct** (right topics, right types) - **Lifecycle methods behave properly** (init, tick, shutdown) - **Edge cases are handled** (no messages, invalid data, etc.) - **Refactoring doesn't break functionality** ## Testing Strategies ### 1. Unit Testing a Single Node Test node behavior without running the scheduler. ### 2. Integration Testing Multiple Nodes Test nodes communicating through the Topic. ### 3. Testing Business Logic in Isolation Extract and test business logic without Topic dependencies. ## Unit Testing a Single Node Test individual node behavior in isolation. ### Example: Testing a Temperature Sensor **File: `src/main.rs`** ```rust use horus::prelude::*; // The node we want to test pub struct TemperatureSensor { temp_pub: Topic, reading: f32, } impl TemperatureSensor { pub fn new() -> Result { Ok(Self { temp_pub: Topic::new("temperature")?, reading: 20.0, }) } // Make this public so tests can inspect it pub fn get_reading(&self) -> f32 { self.reading } } impl Node for TemperatureSensor { fn name(&self) -> &str { "TemperatureSensor" } fn init(&mut self) -> Result<()> { hlog!(info, "Sensor initialized"); Ok(()) } fn tick(&mut self) { // Increment reading each tick self.reading += 0.5; // Publish temperature self.temp_pub.send(self.reading); } fn shutdown(&mut self) -> Result<()> { hlog!(info, "Sensor shutdown"); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(TemperatureSensor::new()?).order(0).build()?; scheduler.run()?; Ok(()) } // ============================================================================ // TESTS // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_sensor_initialization() { // Test that sensor initializes with correct default value let sensor = TemperatureSensor::new().unwrap(); assert_eq!(sensor.get_reading(), 20.0); } #[test] fn test_sensor_init_lifecycle() { let mut sensor = TemperatureSensor::new().unwrap(); // Test init() method let result = sensor.init(); assert!(result.is_ok()); } #[test] fn test_sensor_tick_increments_reading() { let mut sensor = TemperatureSensor::new().unwrap(); // Run tick 5 times for i in 1..=5 { sensor.tick(); // Verify reading increments by 0.5 each tick let expected = 20.0 + (i as f32 * 0.5); assert_eq!(sensor.get_reading(), expected); } } #[test] fn test_sensor_shutdown() { let mut sensor = TemperatureSensor::new().unwrap(); // Test shutdown() method let result = sensor.shutdown(); assert!(result.is_ok()); } } ``` ### Run the Tests ```bash horus test ``` **Expected Output:** ``` running 4 tests test tests::test_sensor_initialization ... ok test tests::test_sensor_init_lifecycle ... ok test tests::test_sensor_tick_increments_reading ... ok test tests::test_sensor_shutdown ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured ``` ### Key Testing Patterns **1. Test Node Creation:** ```rust #[test] fn test_node_creation() { let node = MyNode::new().unwrap(); assert_eq!(node.some_field, expected_value); } ``` **2. Test Initialization:** ```rust #[test] fn test_init() { let mut node = MyNode::new().unwrap(); assert!(node.init().is_ok()); } ``` **3. Test Tick Logic:** ```rust #[test] fn test_tick() { let mut node = MyNode::new().unwrap(); node.tick(); // Verify state changes assert_eq!(node.counter, 1); } ``` **4. Test Shutdown:** ```rust #[test] fn test_shutdown() { let mut node = MyNode::new().unwrap(); assert!(node.shutdown().is_ok()); } ``` ## Testing Multiple Nodes Together Test nodes communicating through topics. ### Example: Publisher-Subscriber Test **File: `src/main.rs`** ```rust use horus::prelude::*; use std::sync::{Arc, Mutex}; // Publisher node pub struct PublisherNode { data_pub: Topic, } impl PublisherNode { pub fn new() -> Result { Ok(Self { data_pub: Topic::new("test_data")?, }) } } impl Node for PublisherNode { fn name(&self) -> &str { "PublisherNode" } fn tick(&mut self) { self.data_pub.send(42.0); } } // Subscriber node that stores received data pub struct SubscriberNode { data_sub: Topic, received: Arc>>, } impl SubscriberNode { pub fn new(received: Arc>>) -> Result { Ok(Self { data_sub: Topic::new("test_data")?, received, }) } } impl Node for SubscriberNode { fn name(&self) -> &str { "SubscriberNode" } fn tick(&mut self) { if let Some(data) = self.data_sub.recv() { self.received.lock().unwrap().push(data); } } } fn main() -> Result<()> { let received = Arc::new(Mutex::new(Vec::new())); let mut scheduler = Scheduler::new(); scheduler.add(PublisherNode::new()?).order(0).build()?; scheduler.add(SubscriberNode::new(received)?).order(1).build()?; scheduler.run()?; Ok(()) } #[cfg(test)] mod tests { use super::*; use std::thread; use std::time::Duration; #[test] fn test_pubsub_communication() { // Shared storage for received messages let received = Arc::new(Mutex::new(Vec::new())); // Create publisher and subscriber let mut pub_node = PublisherNode::new().unwrap(); let mut sub_node = SubscriberNode::new(Arc::clone(&received)).unwrap(); // Publish a message pub_node.tick(); // Small delay to allow IPC (shared memory needs time to propagate) thread::sleep(Duration::from_millis(10)); // Subscriber receives the message sub_node.tick(); // Verify message was received let data = received.lock().unwrap(); assert_eq!(data.len(), 1); assert_eq!(data[0], 42.0); } #[test] fn test_multiple_messages() { let received = Arc::new(Mutex::new(Vec::new())); let mut pub_node = PublisherNode::new().unwrap(); let mut sub_node = SubscriberNode::new(Arc::clone(&received)).unwrap(); // Publish 5 messages for _ in 0..5 { pub_node.tick(); thread::sleep(Duration::from_millis(5)); sub_node.tick(); } // Verify all messages received let data = received.lock().unwrap(); assert_eq!(data.len(), 5); for value in data.iter() { assert_eq!(*value, 42.0); } } } ``` ### Run Integration Tests ```bash horus test test_pubsub_communication --test-threads 1 ``` **Why `--test-threads 1`?** - Prevents tests from running in parallel (this is the default for `horus test`) - Avoids shared memory conflicts between tests - Ensures deterministic behavior **Expected Output:** ``` running 2 tests test tests::test_pubsub_communication ... ok test tests::test_multiple_messages ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured ``` ## Testing Business Logic in Isolation Test node logic without real Topic connections. ### Example: Extracting Testable Logic ```rust use horus::prelude::*; // Node that processes temperature data pub struct TemperatureProcessor { input_sub: Topic, output_pub: Topic, } impl TemperatureProcessor { pub fn new() -> Result { Ok(Self { input_sub: Topic::new("input_temp")?, output_pub: Topic::new("output_temp")?, }) } // Public method for testing business logic pub fn process_temperature(&self, temp: f32) -> f32 { // Convert Celsius to Fahrenheit temp * 9.0 / 5.0 + 32.0 } } impl Node for TemperatureProcessor { fn name(&self) -> &str { "TemperatureProcessor" } fn tick(&mut self) { if let Some(celsius) = self.input_sub.recv() { let fahrenheit = self.process_temperature(celsius); self.output_pub.send(fahrenheit); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_temperature_conversion_logic() { // Test business logic WITHOUT Topic let processor = TemperatureProcessor::new().unwrap(); // Test known conversions assert_eq!(processor.process_temperature(0.0), 32.0); assert_eq!(processor.process_temperature(100.0), 212.0); assert_eq!(processor.process_temperature(-40.0), -40.0); } #[test] fn test_with_mock_data() { let mut processor = TemperatureProcessor::new().unwrap(); // We can't easily mock Topic, but we can test the logic // by calling process_temperature directly let celsius_readings = vec![0.0, 10.0, 20.0, 30.0, 100.0]; let expected_fahrenheit = vec![32.0, 50.0, 68.0, 86.0, 212.0]; for (celsius, expected) in celsius_readings.iter().zip(expected_fahrenheit.iter()) { let result = processor.process_temperature(*celsius); assert_eq!(result, *expected); } } } ``` ### Testing Strategy Without Topic Mocks Since HORUS Topics use real shared memory, full mocking is complex. Instead: **1. Extract Business Logic:** ```rust // Good: Business logic in testable method pub fn process_temperature(&self, temp: f32) -> f32 { temp * 9.0 / 5.0 + 32.0 } // Test this directly without Topic #[test] fn test_logic() { let node = TemperatureProcessor::new().unwrap(); assert_eq!(node.process_temperature(0.0), 32.0); } ``` **2. Test Tick with Real Topics:** ```rust // Topics are lightweight — use real ones in tests #[test] fn test_with_real_topic() { let mut node = MyNode::new().unwrap(); node.tick(); // Verify behavior } ``` **3. Use Shared State for Verification:** ```rust // Store results in node for verification pub struct TestNode { pub last_result: Option, } #[test] fn test_result() { let mut node = TestNode::new(); node.tick(); assert_eq!(node.last_result, Some(42.0)); } ``` ## Complete Testing Example A fully tested 3-node system. **File: `src/main.rs`** ```rust use horus::prelude::*; use std::sync::{Arc, Mutex}; // Node 1: Generate numbers pub struct GeneratorNode { output_pub: Topic, counter: u32, } impl GeneratorNode { pub fn new() -> Result { Ok(Self { output_pub: Topic::new("numbers")?, counter: 0, }) } } impl Node for GeneratorNode { fn name(&self) -> &str { "GeneratorNode" } fn tick(&mut self) { self.counter += 1; self.output_pub.send(self.counter); } } // Node 2: Double the numbers pub struct DoublerNode { input_sub: Topic, output_pub: Topic, } impl DoublerNode { pub fn new() -> Result { Ok(Self { input_sub: Topic::new("numbers")?, output_pub: Topic::new("doubled")?, }) } } impl Node for DoublerNode { fn name(&self) -> &str { "DoublerNode" } fn tick(&mut self) { if let Some(n) = self.input_sub.recv() { self.output_pub.send(n * 2); } } } // Node 3: Collect results pub struct CollectorNode { input_sub: Topic, collected: Arc>>, } impl CollectorNode { pub fn new(collected: Arc>>) -> Result { Ok(Self { input_sub: Topic::new("doubled")?, collected, }) } } impl Node for CollectorNode { fn name(&self) -> &str { "CollectorNode" } fn tick(&mut self) { if let Some(n) = self.input_sub.recv() { self.collected.lock().unwrap().push(n); } } } fn main() -> Result<()> { let collected = Arc::new(Mutex::new(Vec::new())); let mut scheduler = Scheduler::new(); scheduler.add(GeneratorNode::new()?).order(0).build()?; scheduler.add(DoublerNode::new()?).order(1).build()?; scheduler.add(CollectorNode::new(collected)?).order(2).build()?; scheduler.run()?; Ok(()) } #[cfg(test)] mod tests { use super::*; use std::thread; use std::time::Duration; #[test] fn test_generator_node() { let mut node = GeneratorNode::new().unwrap(); // Initial state assert_eq!(node.counter, 0); // After 3 ticks for _ in 0..3 { node.tick(); } assert_eq!(node.counter, 3); } #[test] fn test_pipeline() { let collected = Arc::new(Mutex::new(Vec::new())); let mut gen = GeneratorNode::new().unwrap(); let mut dbl = DoublerNode::new().unwrap(); let mut col = CollectorNode::new(Arc::clone(&collected)).unwrap(); // Run 5 iterations of the pipeline for _ in 0..5 { gen.tick(); thread::sleep(Duration::from_millis(5)); dbl.tick(); thread::sleep(Duration::from_millis(5)); col.tick(); } // Verify results: 1*2=2, 2*2=4, 3*2=6, 4*2=8, 5*2=10 let results = collected.lock().unwrap(); assert_eq!(*results, vec![2, 4, 6, 8, 10]); } } ``` ### Run All Tests ```bash horus test --test-threads 1 ``` **Output:** ``` running 2 tests test tests::test_generator_node ... ok test tests::test_pipeline ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured ``` ## Best Practices ### 1. Test Business Logic Separately Extract pure functions for easy testing: ```rust // Good: Pure function (easy to test) fn calculate_velocity(distance: f32, time: f32) -> f32 { distance / time } #[test] fn test_velocity() { assert_eq!(calculate_velocity(100.0, 10.0), 10.0); } ``` ### 2. Use Arc for Shared Test Data Share data between nodes for verification: ```rust let results = Arc::new(Mutex::new(Vec::new())); let node = TestNode::new(Arc::clone(&results))?; // Later in test assert_eq!(results.lock().unwrap().len(), 5); ``` ### 3. Add Small Delays for IPC Shared memory needs time to propagate: ```rust pub_node.tick(); thread::sleep(Duration::from_millis(10)); // Allow IPC sub_node.tick(); ``` ### 4. Run Tests Sequentially `horus test` defaults to single-threaded execution to prevent shared memory conflicts. If you use `--parallel`, ensure each test uses unique topic names: ```bash horus test # Already single-threaded by default ``` ### 5. Test Edge Cases ```rust #[test] fn test_no_messages() { let mut node = SubscriberNode::new().unwrap(); node.tick(); // Should handle gracefully } #[test] fn test_invalid_data() { let result = node.process(-1.0); assert!(result.is_err()); } ``` ## Running Tests with horus test ### Basic Commands ```bash # Run all tests (defaults to single-threaded for shared memory safety) horus test # Run specific test by name filter horus test test_sensor_initialization # Run tests matching a pattern horus test sensor # Show test output (println!, hlog!, etc.) horus test --nocapture # Run with multiple threads (override default single-threaded mode) horus test --parallel horus test --test-threads 4 # Run in release mode (optimized build) horus test --release # Skip the build step (use existing build artifacts) horus test --no-build # Skip shared memory cleanup after tests horus test --no-cleanup # Verbose output horus test -v # Run integration tests (tests marked #[ignore]) horus test --integration # Enable simulation drivers (no hardware required) horus test --sim ``` ### Test Organization ```rust #[cfg(test)] mod tests { use super::*; mod unit_tests { use super::*; #[test] fn test_creation() { /* ... */ } } mod integration_tests { use super::*; // Mark integration tests with #[ignore] — run with `horus test --integration` #[test] #[ignore] fn test_full_pipeline() { /* ... */ } } } ``` ## Single-Tick Testing with tick_once() For simulation and fine-grained testing, `tick_once()` executes exactly one scheduler tick cycle and returns. This gives you full control over the execution loop. ### tick_once() Execute all registered nodes exactly once in priority order: ```rust #[test] fn test_single_tick_behavior() { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new().unwrap()).order(0).build().unwrap(); scheduler.add(ControlNode::new().unwrap()).order(1).build().unwrap(); // Execute one tick — all nodes run once in priority order scheduler.tick_once().unwrap(); // Verify state after exactly one tick // ... } ``` ### tick() Execute only specific nodes by name. Non-existent names are silently ignored: ```rust #[test] fn test_selective_tick() { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new().unwrap()).order(0).build().unwrap(); scheduler.add(ControlNode::new().unwrap()).order(1).build().unwrap(); scheduler.add(LoggerNode::new().unwrap()).order(2).build().unwrap(); // Tick only the sensor — control and logger are skipped scheduler.tick(&["SensorNode"]).unwrap(); // Tick sensor + control, skip logger scheduler.tick(&["SensorNode", "ControlNode"]).unwrap(); } ``` ### Simulation Loop Pattern Use `tick_once()` to integrate HORUS nodes into a simulation loop: ```rust fn run_simulation() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(MotorController::new()?).order(0).build()?; scheduler.add(SensorFusion::new()?).order(1).build()?; let dt = Duration::from_millis(10); // 100 Hz simulation for step in 0..1000 { // 1. Step physics simulation sim.step(dt); // 2. Run HORUS nodes for this timestep scheduler.tick_once()?; // 3. Render or collect results sim.render(); } Ok(()) } ``` ### Lazy Initialization `tick_once()` and `tick()` lazily initialize nodes on the first call — you don't need to call `run()` or any separate init method. The first `tick_once()` call runs `init()` on all nodes, then executes the tick. ## Time-Limited Test Runs Use `scheduler.run_for()` to run tests for a fixed duration: ```rust #[test] fn test_system_runs_for_one_second() { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new().unwrap()).order(0).build().unwrap(); scheduler.add(ControlNode::new().unwrap()).order(1).build().unwrap(); // Run for exactly 1 second, then shutdown gracefully scheduler.run_for(std::time::Duration::from_secs(1)).unwrap(); } ``` ## Record/Replay Testing HORUS supports recording node execution and replaying it later for deterministic debugging. This is useful for reproducing bugs and regression testing. ### Recording a Session Record all node inputs/outputs during a run: ```bash # Record while running horus run --record ``` Recordings are saved to `~/.horus/recordings//` in binary format (`.horus` files). Each node gets its own recording file: ``` ~/.horus/recordings/my_session/ ├── sensor_node@abc123.horus # Node recording ├── control_node@def456.horus # Node recording └── scheduler@main789.horus # Scheduler execution order ``` ### Replaying in Tests Use `scheduler.add_replay()` to replay a recorded node: ```rust use std::path::PathBuf; #[test] fn test_replay_crash_scenario() { let mut scheduler = Scheduler::new(); // Replay the motor node from a crash recording scheduler.add_replay( PathBuf::from("~/.horus/recordings/crash/motor_node@abc123.horus"), 1, // priority ).unwrap(); // Add a live node to test against the recorded data scheduler.add(DiagnosticNode::new().unwrap()).order(2).build().unwrap(); scheduler.run_for(std::time::Duration::from_secs(5)).unwrap(); } ``` ### Replay Modes | Mode | Description | |------|-------------| | **Full replay** | Replay all nodes from a scheduler recording | | **Mixed replay** | Replay some nodes while others run live | | **Range replay** | Replay only a specific tick range | ### Managing Recordings ```bash # List recording sessions ls ~/.horus/recordings/ # Recordings auto-cap at 100MB per node # Delete old recordings to free space ``` ## Troubleshooting Tests ### Issue: Tests Fail Randomly **Cause:** Shared memory conflicts from parallel tests **Fix:** ```bash # horus test defaults to single-threaded, but if you used --parallel: horus test --test-threads 1 ``` ### Issue: "Topic not found" Errors **Cause:** Topic created in one test affects another **Fix:** Use unique topic names per test: ```rust Topic::new("test_topic_1")? // Test 1 Topic::new("test_topic_2")? // Test 2 ``` ### Issue: Messages Not Received **Cause:** IPC needs time to propagate **Fix:** Add small delay: ```rust thread::sleep(Duration::from_millis(10)); ``` ## Next Steps - **[Examples](/rust/examples/basic-examples)** - See complete tested applications - **[Core Concepts](/concepts/core-concepts-nodes)** - Understand node lifecycle - **[Monitor](/development/monitor)** - Debug with visual monitoring - **[CLI Reference](/development/cli-reference#horus-test)** - Full `horus test` command reference ## See Also - [CLI: horus test](/development/cli-reference) - Full test command reference - [Scheduler tick_once()](/rust/api/scheduler) - Single-tick execution for testing - [Deterministic Mode](/advanced/deterministic-mode) - Reproducible test runs --- ## Parameters Guide Path: /development/parameters Description: Configure and tune your HORUS applications with runtime parameters # Parameters Guide Runtime parameters in HORUS provide **dynamic configuration** without recompiling code. Adjust speeds, gains, thresholds, and behaviors on-the-fly for rapid prototyping and tuning. ## Why Parameters? **Without parameters:** ```rust // Hardcoded - requires recompile to change let max_speed = 1.5; let pid_kp = 1.0; ``` **With parameters:** ```rust // Dynamic - change at runtime via monitor or CLI let max_speed = self.params.get_or("max_speed", 1.5); let pid_kp = self.params.get_or("pid_kp", 1.0); ``` **Benefits:** - **No recompilation** - Change values without rebuilding - **Live tuning** - Adjust while robot is running - **Persistence** - Save/load from YAML files - **Sharing** - Export/import parameter sets - **Safety** - Fallback to defaults if missing - **Validation** - Range, regex, enum, and read-only constraints - **Versioning** - Optimistic locking for concurrent edits ## Core Concepts ### Parameter Storage Parameters are stored in a **thread-safe map**: ```rust Arc>> ``` **Location:** `.horus/config/params.yaml` (relative to your project directory) **Format:** ```yaml # Flat key-value pairs (keys are plain strings) tick_rate: 30 max_memory_mb: 512 max_speed: 1.0 max_angular_speed: 1.0 acceleration_limit: 0.5 lidar_rate: 10 camera_fps: 30 sensor_timeout_ms: 1000 emergency_stop_distance: 0.3 collision_threshold: 0.5 pid_kp: 1.0 pid_ki: 0.1 pid_kd: 0.05 ``` ### Parameter Types HORUS supports all JSON-compatible types: - **Numbers** - `f64`, `i64`, `u64` (stored as `Value::Number`) - **Strings** - `String` (stored as `Value::String`) - **Booleans** - `bool` (stored as `Value::Bool`) - **Arrays** - `Vec` (stored as `Value::Array`) - **Objects** - `HashMap` (stored as `Value::Object`) ### Key Organization Keys are stored as **flat strings** in a `BTreeMap` (sorted alphabetically). Use descriptive names with underscores: ```rust // Descriptive flat keys self.params.get_or("max_speed", 1.5); self.params.get_or("pid_kp", 1.0); self.params.get_or("lidar_rate", 10); ``` You can use dot notation as a naming convention for grouping, but note that dots are treated as literal characters — there is no automatic hierarchy: ```rust // Dot notation is a naming convention, not a hierarchy self.params.get_or("motion.max_speed", 1.5); self.params.get_or("control.pid.kp", 1.0); ``` ## Using Parameters in Nodes ### Accessing Parameters Store a `RuntimeParams` instance as a field on your node struct: ```rust use horus::prelude::*; pub struct VelocityController { params: RuntimeParams, max_speed: f64, acceleration: f64, } impl VelocityController { fn new() -> Result { let params = RuntimeParams::init()?; let max_speed = params.get_or("max_speed", 1.5); let acceleration = params.get_or("acceleration_limit", 0.5); Ok(Self { params, max_speed, acceleration }) } } impl Node for VelocityController { fn name(&self) -> &str { "velocity_controller" } fn init(&mut self) -> Result<()> { self.max_speed = self.params.get_or("max_speed", 1.5); self.acceleration = self.params.get_or("acceleration_limit", 0.5); hlog!(info, "Max speed: {} m/s", self.max_speed); hlog!(info, "Acceleration: {} m/s²", self.acceleration); Ok(()) } fn tick(&mut self) { let target_velocity = self.max_speed; // Use parameters... } } ``` ### Parameter Methods **Generic get with default (`get_or`):** ```rust let speed = self.params.get_or("max_speed", 1.5); // f64 let enabled = self.params.get_or("auto_mode", false); // bool let rate = self.params.get_or("update_rate", 60); // i32 let name = self.params.get_or("node_name", "default"); // String ``` **Generic get (returns `Option`):** ```rust // Returns None if parameter doesn't exist or type doesn't match if let Some(speed) = self.params.get::("max_speed") { self.max_speed = speed; } ``` **Generic get with default:** ```rust // Returns default if parameter doesn't exist let rate: i32 = self.params.get_or("update_rate", 60); ``` **Set parameter:** ```rust // Update parameter value (validates against metadata if set) self.params.set("max_speed", 2.0)?; // Set complex types self.params.set("camera_resolution", vec![1920, 1080])?; ``` **Query methods:** ```rust self.params.has("max_speed"); // bool — check if key exists self.params.list_keys(); // Vec — all parameter keys self.params.get_all(); // BTreeMap — all params self.params.remove("old_key"); // Option — remove and return self.params.reset()?; // Reset all params to defaults ``` **Persistence:** ```rust // Save current params to .horus/config/params.yaml self.params.save_to_disk()?; // Load params from a specific YAML file self.params.load_from_disk(Path::new("my_params.yaml"))?; ``` ### Live Reloading **Check for updates every tick:** ```rust pub struct AdaptiveController { params: RuntimeParams, max_speed: f64, tick_count: u64, } impl Node for AdaptiveController { fn name(&self) -> &str { "adaptive_controller" } fn tick(&mut self) { // Check every 60 ticks (~1 second at 60 Hz) if self.tick_count % 60 == 0 { let new_speed = self.params.get_or("max_speed", 1.5); if new_speed != self.max_speed { hlog!(info, "Speed updated: {} → {}", self.max_speed, new_speed); self.max_speed = new_speed; } } self.tick_count += 1; } } ``` **Performance note:** Parameter access is fast (~80-350ns via `Arc`), but avoid reading hundreds of parameters every tick. Cache values and reload periodically. ### Complex Parameter Types **Arrays:** ```rust // Set array self.params.set("waypoints", vec![1.0, 2.5, 3.0, 4.5])?; // Get array let waypoints: Vec = self.params .get::>("waypoints") .map(|v| v.iter().filter_map(|x| x.as_f64()).collect()) .unwrap_or_default(); ``` **Objects:** ```rust use serde_json::json; // Set nested object let config = json!({ "ip": "192.168.1.100", "port": 8080, "timeout_ms": 5000 }); self.params.set("network_config", config)?; // Get the object back if let Some(config) = self.params.get::>("network_config") { let ip = config.get("ip").and_then(|v| v.as_str()).unwrap_or("localhost"); let port = config.get("port").and_then(|v| v.as_i64()).unwrap_or(8080); } ``` ## Default Parameters When `RuntimeParams::init()` is called and no `.horus/config/params.yaml` file exists, HORUS provides these defaults: ```yaml # .horus/config/params.yaml (auto-generated defaults) # System tick_rate: 30 max_memory_mb: 512 # Motion max_speed: 1.0 max_angular_speed: 1.0 acceleration_limit: 0.5 # Sensors lidar_rate: 10 camera_fps: 30 sensor_timeout_ms: 1000 # Safety emergency_stop_distance: 0.3 collision_threshold: 0.5 # PID pid_kp: 1.0 pid_ki: 0.1 pid_kd: 0.05 ``` **Customization:** 1. Defaults are loaded if no params file exists in the project 2. Edit the YAML file directly, use the monitor, or use `horus param set` 3. Call `params.reset()` to restore defaults 4. Call `params.save_to_disk()` to persist changes ## Managing Parameters ### Via Monitor **Web interface** (easiest method): ```bash # Start monitor horus monitor # Navigate to Parameters tab # View all parameters # Edit values inline # Changes auto-save to disk ``` **Features:** - Live editing with validation - Type indicators (number/string/boolean) - Export entire parameter set - Import from YAML/JSON - Delete individual parameters See [Monitor Guide](/development/monitor#tune-parameters-live) for API details. ### Via Code **Save to disk:** ```rust // Parameters are NOT auto-saved on set() — you must save explicitly self.params.save_to_disk()?; ``` **Load from disk:** ```rust use std::path::Path; // Load from a specific file self.params.load_from_disk(Path::new(".horus/config/params.yaml"))?; ``` ### Via CLI Use `horus param` to manage parameters from the command line: ```bash # List all parameters horus param list horus param list --verbose # Include metadata horus param list --json # JSON output # Get/set values horus param get max_speed horus param set max_speed 2.0 horus param set enabled true # Delete a parameter horus param delete old_key # Reset all parameters to defaults horus param reset horus param reset --force # Skip confirmation # Save/load from files horus param save -o my_preset.yaml horus param load my_preset.yaml # Dump all parameters as YAML to stdout horus param dump ``` ### Via File Edit **Direct YAML editing:** ```bash # Edit parameters file (project-relative) vim .horus/config/params.yaml # Changes take effect on next RuntimeParams::init() or load_from_disk() ``` **Format:** ```yaml # Use spaces (2 or 4), not tabs max_speed: 2.0 # number mode: "auto" # string (quotes optional for simple strings) enabled: true # boolean rates: [10, 30, 100] # array # Comments are preserved pid_kp: 1.0 # Proportional gain pid_ki: 0.1 # Integral gain pid_kd: 0.05 # Derivative gain ``` ## Common Patterns ### PID Controller Tuning ```rust use horus::prelude::*; pub struct PIDController { params: RuntimeParams, kp: f64, ki: f64, kd: f64, integral: f64, last_error: f64, } impl Node for PIDController { fn name(&self) -> &str { "pid_controller" } fn init(&mut self) -> Result<()> { self.kp = self.params.get_or("pid_kp", 1.0); self.ki = self.params.get_or("pid_ki", 0.1); self.kd = self.params.get_or("pid_kd", 0.05); hlog!(info, "PID: Kp={}, Ki={}, Kd={}", self.kp, self.ki, self.kd); Ok(()) } fn tick(&mut self) { let error = self.compute_error(); self.integral += error; let derivative = error - self.last_error; let output = self.kp * error + self.ki * self.integral + self.kd * derivative; self.last_error = error; // Use output... } } impl PIDController { fn compute_error(&self) -> f64 { // Your error calculation 0.0 } } ``` **Tuning workflow:** 1. Start robot with default gains 2. Open monitor → Parameters 3. Adjust `pid_kp`/`pid_ki`/`pid_kd` while robot runs 4. Observe behavior in monitor metrics 5. Repeat until satisfactory 6. Save with `horus param save` or `params.save_to_disk()` ### Feature Flags ```rust pub struct AdvancedController { params: RuntimeParams, enable_obstacle_avoidance: bool, enable_path_planning: bool, enable_localization: bool, } impl Node for AdvancedController { fn name(&self) -> &str { "advanced_controller" } fn init(&mut self) -> Result<()> { self.enable_obstacle_avoidance = self.params.get_or("obstacle_avoidance", false); self.enable_path_planning = self.params.get_or("path_planning", false); self.enable_localization = self.params.get_or("localization", true); Ok(()) } fn tick(&mut self) { if self.enable_localization { self.update_localization(); } if self.enable_obstacle_avoidance { self.avoid_obstacles(); } if self.enable_path_planning { self.plan_path(); } } } impl AdvancedController { fn update_localization(&mut self) { /* ... */ } fn avoid_obstacles(&mut self) { /* ... */ } fn plan_path(&mut self) { /* ... */ } } ``` ### Environment-Specific Config ```rust pub struct NetworkNode { params: RuntimeParams, server_url: String, timeout_ms: u64, } impl Node for NetworkNode { fn name(&self) -> &str { "network_node" } fn init(&mut self) -> Result<()> { let env: String = self.params.get_or("environment", "development".to_string()); match env.as_str() { "production" => { self.server_url = self.params.get_or("prod_url", "prod.example.com:8080".to_string()); self.timeout_ms = self.params.get_or("prod_timeout_ms", 3000_i64) as u64; }, "staging" => { self.server_url = self.params.get_or("staging_url", "staging.example.com:8080".to_string()); self.timeout_ms = self.params.get_or("staging_timeout_ms", 5000_i64) as u64; }, _ => { self.server_url = self.params.get_or("dev_url", "localhost:8080".to_string()); self.timeout_ms = self.params.get_or("dev_timeout_ms", 10000_i64) as u64; } } hlog!(info, "Connecting to {} (timeout: {}ms)", self.server_url, self.timeout_ms); Ok(()) } fn tick(&mut self) { // Network logic... } } ``` ### Rate Limiting ```rust pub struct SensorPublisher { params: RuntimeParams, publish_rate: u64, last_publish: std::time::Instant, } impl Node for SensorPublisher { fn name(&self) -> &str { "sensor_publisher" } fn init(&mut self) -> Result<()> { self.publish_rate = self.params.get_or("publish_rate", 10_i64) as u64; hlog!(info, "Publishing at {} Hz", self.publish_rate); Ok(()) } fn tick(&mut self) { let interval = std::time::Duration::from_millis(1000 / self.publish_rate); if self.last_publish.elapsed() >= interval { self.publish_data(); self.last_publish = std::time::Instant::now(); } } } impl SensorPublisher { fn publish_data(&mut self) { // Publishing logic } } ``` ## Best Practices ### Naming Conventions **Use descriptive names:** ```yaml # Good lidar_scan_rate: 10 camera_resolution_width: 1920 # Bad rate: 10 w: 1920 ``` **Use consistent snake_case:** ```yaml # Good (snake_case) max_speed: 1.5 acceleration_limit: 0.5 # Bad (mixed casing) maxSpeed: 1.5 acceleration_limit: 0.5 ``` ### Always Provide Defaults **Never crash on missing parameters:** ```rust // Good - provides fallback let speed = self.params.get_or("max_speed", 1.5); // Bad - panics if missing let speed = self.params.get::("max_speed").unwrap(); ``` **Use sensible defaults:** ```rust // Good - safe defaults let emergency_stop = self.params.get_or("emergency_stop", true); // Default to safe state let max_speed = self.params.get_or("max_speed", 1.0); // Default to slow // Bad - unsafe defaults let emergency_stop = self.params.get_or("emergency_stop", false); // Unsafe! let max_speed = self.params.get_or("max_speed", 100.0); // Too fast! ``` ### Document Parameters **Add comments in YAML:** ```yaml # Maximum linear velocity in m/s (default: 1.0) max_speed: 1.0 # Maximum angular velocity in rad/s (default: 1.0) max_angular_speed: 1.0 # Proportional gain - affects responsiveness (range: 0.1-10.0) pid_kp: 1.0 # Integral gain - affects steady-state error (range: 0.01-1.0) pid_ki: 0.1 # Derivative gain - affects damping (range: 0.001-0.1) pid_kd: 0.05 ``` **Add documentation in code:** ```rust fn init(&mut self) -> Result<()> { // Load PID gains (tuning range: Kp=0.1-10, Ki=0.01-1, Kd=0.001-0.1) self.kp = self.params.get_or("pid_kp", 1.0); self.ki = self.params.get_or("pid_ki", 0.1); self.kd = self.params.get_or("pid_kd", 0.05); Ok(()) } ``` ### Validate Parameter Values **Use built-in validation rules:** RuntimeParams supports metadata with validation rules that are checked on `set()`: ```rust use horus::prelude::*; // Provides RuntimeParams, ParamMetadata, ValidationRule let params = RuntimeParams::init()?; // Set validation rules for a parameter params.set_metadata("max_speed", ParamMetadata { description: Some("Maximum robot speed".to_string()), unit: Some("m/s".to_string()), validation: vec![ValidationRule::Range(0.0, 5.0)], read_only: false, })?; // This succeeds: params.set("max_speed", 2.0)?; // This returns an error (out of range): params.set("max_speed", 10.0)?; // Error! ``` **Available validation rules:** | Rule | Description | |------|-------------| | `MinValue(f64)` | Minimum numeric value | | `MaxValue(f64)` | Maximum numeric value | | `Range(f64, f64)` | Numeric range (min, max) | | `RegexPattern(String)` | String must match regex | | `Enum(Vec)` | Value must be one of allowed strings | | `MinLength(usize)` | Minimum string/array length | | `MaxLength(usize)` | Maximum string/array length | | `RequiredKeys(Vec)` | Object must contain these keys | **Read-only parameters:** ```rust params.set_metadata("version", ParamMetadata { description: Some("System version".to_string()), unit: None, validation: vec![], read_only: true, })?; // This returns an error: params.set("version", "1.0.0")?; // Error: Parameter 'version' is read-only ``` **Manual bounds checking in code:** ```rust fn init(&mut self) -> Result<()> { let speed = self.params.get_or("max_speed", 1.5); // Clamp to safe range self.max_speed = speed.max(0.0).min(5.0); if speed != self.max_speed { hlog!(warn, "max_speed {} out of range, clamped to {}", speed, self.max_speed); } Ok(()) } ``` ### Export Parameter Sets **Create presets for different scenarios:** ```bash # Save current parameters to a preset file horus param save -o aggressive_tuning.yaml # Backup current params and switch to a different preset cp .horus/config/params.yaml .horus/config/params_backup.yaml horus param load aggressive_tuning.yaml # Dump current params to stdout for inspection horus param dump ``` ## Troubleshooting ### Parameters Not Loading **Problem:** Parameters show default values even though YAML file exists. **Cause:** YAML syntax error, wrong file location, or file permissions. **Solution:** ```bash # Check file exists in the right place (project-relative) ls -la .horus/config/params.yaml # Validate YAML syntax yamllint .horus/config/params.yaml # Check permissions chmod 644 .horus/config/params.yaml # Verify with CLI horus param list ``` ### Parameters Not Saving **Problem:** Changes via `set()` don't persist after restart. **Cause:** `set()` only updates in-memory storage. You must call `save_to_disk()` explicitly. **Solution:** ```bash # Create config directory if needed mkdir -p .horus/config # Save from CLI horus param save # Or save from code self.params.save_to_disk()?; ``` ### Type Mismatch **Problem:** Parameter exists but wrong type. **Error:** ``` Parameter 'motion.max_speed' expected f64, got String ``` **Solution:** Check YAML format: ```yaml # Wrong - string max_speed: "1.5" # Correct - number max_speed: 1.5 ``` Or use type conversion in code: ```rust // Try as number first, then parse string as fallback let speed = self.params.get_or("max_speed", 1.5); ``` ### Lost Parameters After Update **Problem:** Parameters reset to defaults after code update. **Cause:** If `.horus/config/params.yaml` is deleted or empty, `RuntimeParams::init()` loads defaults. **Solution:** Backup parameters before updating: ```bash # Backup horus param save -o params_backup.yaml # After update, restore if needed horus param load params_backup.yaml ``` ## Performance Considerations ### Access Speed Parameters use `Arc>`: - **Read**: ~80-350ns (read lock + BTreeMap lookup) - **Write**: ~100-500ns (write lock + BTreeMap insert + potential save) - **Thread-safe**: Multiple nodes can read simultaneously **Fast enough for:** - Loading parameters in `init()` (one-time) - Checking parameters every tick (60 Hz) - Checking parameters every 100 ticks (~1 second) **Too slow for:** - Reading hundreds of parameters every tick - Using as real-time message passing (use buffered `Topic` instead) ### Caching Strategy **Good: Cache and reload periodically** ```rust fn tick(&mut self) { // Reload every 60 ticks (~1 second) if self.reload_counter % 60 == 0 { self.max_speed = self.params.get_or("max_speed", 1.5); } self.reload_counter += 1; // Use cached value let velocity = calculate_velocity(self.max_speed); } ``` **Bad: Read every tick unnecessarily** ```rust fn tick(&mut self) { // Wasteful - reads same value 60 times per second let max_speed = self.params.get_or("max_speed", 1.5); } ``` ## Version Tracking RuntimeParams includes an optimistic locking system for concurrent edit protection: ```rust // Get current version of a parameter let version = self.params.get_version("max_speed"); // Set with version check — fails if another writer changed it self.params.set_with_version("max_speed", 2.0, version)?; ``` This prevents lost updates when multiple processes or threads modify the same parameter simultaneously. ## Audit Logging Parameter changes are automatically logged to `.horus/logs/param_changes.log`: ``` [2025-01-15 14:30:00] max_speed: 1.0 -> 2.0 [2025-01-15 14:31:15] pid_kp: 1.0 -> 1.5 ``` This provides a history of all runtime parameter modifications for debugging and tuning review. ## Next Steps - **[Monitor Guide](/development/monitor)** - Manage parameters via web interface - **[Core Concepts](/concepts/core-concepts-nodes)** - Learn about nodes, topics, and scheduling - **[Examples](/rust/examples/basic-examples)** - See real-world parameter usage - **[CLI Reference](/development/cli-reference#horus-param)** - `horus param` command reference ## See Also - [RuntimeParams API](/rust/api/runtime-params) - Complete API reference for RuntimeParams - [CLI: horus param](/development/cli-reference) - Command-line parameter management - [Monitor](/development/monitor) - Live parameter tuning via web interface --- ## Static Analysis Path: /development/static-analysis Description: Project validation and code checking with horus check # Static Analysis `horus check` validates your HORUS project configuration, Rust code, and Python scripts before running. ## Quick Start ```bash # Run all checks on your project horus check # Check a specific project directory horus check ./my_robot ``` ## What It Checks ### Phase 1: Manifest Validation Validates your `horus.toml` project manifest: | Check | Description | |-------|-------------| | **Project name** | Must be present, lowercase alphanumeric + hyphens/underscores | | **Version** | Must be valid semver | | **Build file** | Detects `Cargo.toml` (Rust) or `pyproject.toml` (Python) | | **Required fields** | Ensures all mandatory fields are present | ```bash horus check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Phase 1: Manifest Validation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✓ Project name valid ✓ Version is valid semver ✓ Build file detected (Cargo.toml) ✓ Configuration valid ``` ### Phase 2: Rust Deep Check For Rust projects, runs `cargo check` to verify code compiles: ```bash ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Phase 2: Rust Deep Check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Running cargo check... ✓ Rust code compiles successfully ``` This catches type errors, borrow checker issues, and missing imports before runtime. ### Phase 3: Python Validation For Python projects, checks syntax and imports: | Check | Description | |-------|-------------| | **Syntax** | Runs `py_compile` on all `.py` files | | **Imports** | Verifies that imported modules are available | ```bash ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Phase 3: Python Validation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Checking Python files... ✓ main.py - syntax OK ✓ nodes/sensor.py - syntax OK ✓ All imports resolvable ``` ### Additional Checks `horus check` also validates: - **Toolchain**: Verifies Rust toolchain and Python interpreter are available - **System requirements**: Checks disk space and system dependencies - **API usage**: Validates HORUS API usage patterns in project code - **Registry connectivity**: Tests connection to the HORUS package registry (if packages are declared) ## Example Output ```bash $ horus check ╔══════════════════════════════════════════╗ ║ HORUS Project Check ║ ╚══════════════════════════════════════════╝ Project: my_robot (v0.1.0) Language: python Phase 1: Manifest ...................... ✓ Phase 2: Python Validation ............ ✓ Phase 3: System Requirements .......... ✓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Result: All checks passed ✓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` ## When to Use Run `horus check` before: - Deploying to a robot - Publishing a package to the registry - After modifying `horus.toml` - After adding new dependencies ## See Also - [CLI Reference](/development/cli-reference) - Full command-line reference - [Getting Started](/getting-started/quick-start) - Project setup guide --- ## CLI Reference Path: /development/cli-reference Description: Complete guide to all HORUS commands # CLI Reference The `horus` command gives you everything you need to build, run, and manage your applications. ## Quick Reference ```bash # Project Management horus init # Initialize workspace in current directory horus new # Create a new project horus run [files...] # Build and run your app horus build [files...] # Build without running horus test [filter] # Run tests horus check [path] # Validate horus.toml and workspace horus clean # Clean build artifacts and shared memory horus lock # Generate or verify horus.lock horus launch # Launch multiple nodes from YAML # Monitoring & Debugging horus monitor [port] # Monitor your system (web or TUI) horus topic # Topic introspection (list, echo, info, hz, pub) horus node # Node management (list, info, kill, restart, pause, resume) horus service # Service interaction (list, call, info, find) horus action # Action introspection (list, info, send-goal, cancel-goal) horus log [node] # View and filter logs horus blackbox # Inspect BlackBox flight recorder (alias: bb) # Coordinate Frames horus frame # Transform Frames (list, echo, tree, info, can, hz) (alias: tf) # Dependencies horus add # Add a dependency to horus.toml (auto-detects source) horus remove # Remove a dependency from horus.toml # Package Management horus install # Install a standalone package or plugin from registry horus uninstall # Uninstall a standalone package or plugin horus list [query] # List/search packages (--global, --all, --json) horus search # Search for available packages/plugins (--category, --json) horus info # Show detailed info about a package or plugin (--json) horus update [package] # Update project dependencies (--dry-run) horus publish # Publish current package to registry (--dry-run) horus unpublish # Unpublish a package from registry (--yes) horus yank # Yank a package version (--reason) horus deprecate # Mark a package as deprecated (--message) horus owner # Manage package owners (list, add, remove, transfer) horus cache # Cache management (info, list, clean, purge) # Parameters & Messages horus param # Parameter management (get, set, list, delete, reset, dump, load, save) horus msg # Message type introspection (list, info, hash) # Development horus fmt # Format code (Rust + Python) horus lint # Lint code (clippy + ruff/pylint) horus doc # Generate documentation horus bench [filter] # Run benchmarks horus deps # Dependency insight (tree, why, outdated, audit) # Maintenance horus doctor # Comprehensive ecosystem health check horus self update # Update the horus CLI to latest version horus config # View/edit horus.toml settings (get, set, list) horus migrate # Migrate project to unified horus.toml format horus sync # Synchronize development environment (--check) # Advanced horus deploy [target] # Deploy to remote robot horus record # Record/replay for debugging and testing horus scripts [name] # Run a script from horus.toml [scripts] # Plugins horus plugin # Plugin management (enable, disable, verify) # Auth horus auth # Authentication (login, api-key, signing-key, logout, whoami) ``` --- ## `horus init` - Initialize Workspace **What it does**: Initializes a HORUS workspace in the current directory, creating the necessary configuration files. **Why it's useful**: Quickly set up an existing directory as a HORUS project without creating new files from templates. ### Basic Usage ```bash # Initialize in current directory (uses directory name) horus init # Initialize with custom name horus init --name my_robot ``` ### All Options ```bash horus init [OPTIONS] Options: -n, --name Workspace name (defaults to directory name) ``` ### Examples **Initialize existing code as HORUS project**: ```bash cd ~/my-robot-code horus init # Creates horus.toml with project configuration ``` **Initialize with specific name**: ```bash horus init --name sensor_array ``` ### What Gets Created Running `horus init` creates: - `horus.toml` - Project config with name and version This is useful when you have existing code and want to add HORUS support, or when setting up a workspace that will contain multiple HORUS projects. --- ## `horus new` - Create Projects **What it does**: Creates a new HORUS project with all the boilerplate set up for you. **Why it's useful**: Minimal configuration required. Select a language and begin development. ### Basic Usage ```bash # Interactive mode (asks you questions) horus new my_project # Rust with node! macro (recommended for reduced boilerplate) horus new my_project --macro # Python project horus new my_project --python ``` ### All Options ```bash horus new [OPTIONS] Options: -m, --macro Rust with node! macro (less boilerplate) -r, --rust Plain Rust project -p, --python Python project -o, --output Where to create it (default: current directory) ``` ### Examples **Start with Rust + macros** (easiest): ```bash horus new temperature_monitor --macro cd temperature_monitor horus run ``` **Python for prototyping**: ```bash horus new sensor_test --python cd sensor_test python main.py ``` **Put it somewhere specific**: ```bash horus new robot_controller --output ~/projects/robots ``` --- ## `horus run` - Build and Run **What it does**: Compiles your code and runs it. Handles all the build tools for you. **Why it's useful**: One command works for Rust and Python. Language is auto-detected from native build files (`Cargo.toml` for Rust, `pyproject.toml` for Python). For Rust, it builds with Cargo. For Python, it handles the appropriate tooling. ### Basic Usage ```bash # Run current directory (finds main.rs or main.py) horus run # Run specific file horus run src/controller.rs # Run optimized (release mode) horus run --release ``` ### All Options ```bash horus run [FILES...] [OPTIONS] [-- ARGS] Options: -r, --release Optimize for speed (recommended for benchmarks) -c, --clean Remove cached build artifacts and dependencies (Use after updating HORUS or when compilation fails) -q, --quiet Suppress progress indicators -d, --drivers Override detected drivers (comma-separated) Example: --drivers camera,lidar,imu -e, --enable Enable capabilities (comma-separated) Example: --enable cuda,editor,python --record Enable recording for this session -- Arguments for your program ``` ### Using --enable for Capabilities The `--enable` flag lets you quickly enable features without editing `horus.toml`: ```bash # Enable CUDA GPU acceleration horus run --enable cuda # Enable multiple capabilities horus run --enable cuda,editor,python # Combine with hardware features horus run --enable gpio,i2c --release ``` **Available capabilities:** | Capability | Description | |------------|-------------| | `cuda`, `gpu` | CUDA GPU acceleration | | `editor` | Scene editor UI | | `python`, `py` | Python bindings | | `headless` | No rendering (for training) | | `gpio`, `i2c`, `spi`, `can`, `serial` | Hardware interfaces | | `opencv` | OpenCV backend | | `realsense` | Intel RealSense support | | `full` | All features | **Or configure in horus.toml:** ```toml enable = ["cuda", "editor"] ``` ### Why --release Matters Debug builds have significantly higher overhead than release builds due to runtime checks and lack of optimizations. **Debug mode** (default): Fast compilation, slower execution - Use case: Development iteration - Typical tick time: 60-200μs - Includes overflow checks, bounds checking, assertions **Release mode** (`--release`): Slower compilation, optimized execution - Use case: Performance testing, benchmarks, production deployment - Typical tick time: 1-3μs - Full compiler optimizations enabled **Common Mistake:** ```bash horus run # Debug mode # You see: [IPC: 1862ns | Tick: 87μs] - Looks slow! horus run --release # Release mode # You see: [IPC: 947ns | Tick: 2μs] - Actually fast! ``` **The tick time difference is dramatic:** - Debug: 60-200μs per tick (too slow for real-time control) - Release: 1-3μs per tick (production-ready performance) **Rule of thumb:** Always use `--release` when: - Measuring performance - Running benchmarks - Testing real-time control loops - Deploying to production - Wondering "why is HORUS slow?" ### Why --clean Matters The `--clean` flag removes the `.horus/target/` directory, which contains cached build artifacts and dependencies. **When to use `--clean`:** 1. **After updating HORUS** - Most common use case ```bash # You updated horus CLI to a new version horus run --clean ``` This fixes version mismatch errors like: ``` error: the package `horus` depends on `horus_core 0.1.0`, but `horus_core 0.1.3` is installed ``` 2. **Compilation fails mysteriously** ```bash horus run --clean # Sometimes cached state gets corrupted ``` 3. **Dependencies changed** ```bash # You modified Cargo.toml dependencies horus run --clean ``` **What it does:** - Removes `.horus/target/` (build artifacts) - Removes cached lock files - Forces fresh dependency resolution - Next build rebuilds everything from scratch **Trade-off:** - First build after `--clean` is slower (5-30 seconds) - Subsequent builds are fast again (incremental compilation) **Note:** The `--clean` flag only affects the current project's `.horus/` directory, not the global `~/.horus/` cache. ### Examples **Daily development**: ```bash horus run # Fast iteration, slower execution ``` **Testing performance**: ```bash horus run --release # See real speed ``` **Build for CI without running** (use `horus build`): ```bash horus build --release ``` **Fresh build** (when things act weird or after updating HORUS): ```bash horus run --clean --release ``` **After updating HORUS CLI** (fixes version mismatch errors): ```bash # Clean removes cached dependencies from .horus/target/ horus run --clean ``` **Pass arguments to your program**: ```bash horus run -- --config robot.yaml --verbose ``` ### Important: Single-File Projects Only `horus run` is designed for **single-file HORUS projects** (main.rs or main.py). It creates a temporary workspace in `.horus/` and automatically handles dependencies. **What works with `horus run`:** - Single main.rs with all nodes defined in one file - Simple Python scripts (main.py) **What doesn't work:** - Multi-crate Cargo workspaces - Projects with multiple Cargo.toml files - Complex module structures with separate crate directories **For multi-crate projects**, use `cargo` directly: ```bash cd your_multi_crate_project cargo build --release cargo run --release ``` **Example of a proper single-file structure:** ```rust // main.rs - everything in one file use horus::prelude::*; struct SensorNode { /* ... */ } impl Node for SensorNode { /* ... */ } struct ControlNode { /* ... */ } impl Node for ControlNode { /* ... */ } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); scheduler.add(SensorNode::new()?).order(0).build()?; scheduler.add(ControlNode::new()?).order(1).build()?; scheduler.run() } ``` ### Concurrent Multi-Process Execution HORUS supports running multiple node files concurrently as separate processes using **glob patterns**. This is ideal for distributed robotics systems where nodes need to run independently. **Basic Usage:** ```bash horus run "nodes/*.py" # Run all Python nodes concurrently horus run "src/*.rs" # Run all Rust nodes concurrently ``` **How it works:** 1. **Phase 1 (Build)**: Builds all files sequentially, respecting Cargo's file lock 2. **Phase 2 (Execute)**: Spawns all processes concurrently with their own schedulers 3. Each process communicates via HORUS shared memory IPC **Features:** - -**Color-coded output**: Each node is prefixed with `[node_name]` in a unique color - -**Graceful shutdown**: Ctrl+C cleanly terminates all processes - -**Multi-language**: Works with Rust and Python - -**Automatic detection**: No flags needed, just use glob patterns **Example output:** ```bash $ horus run "nodes/*.py" Executing 3 files concurrently: 1. nodes/sensor.py (python) 2. nodes/controller.py (python) 3. nodes/logger.py (python) Phase 1: Building all files... Phase 2: Starting all processes... Started [sensor] Started [controller] Started [logger] All processes running. Press Ctrl+C to stop. [sensor] Sensor reading: 25.3°C [controller] Motor speed: 45% [logger] System operational [sensor] Sensor reading: 26.1°C [controller] Motor speed: 50% [logger] System operational ``` **When to use concurrent execution:** - Multi-node systems where each node is in a separate file - Distributed control architectures (similar to ROS nodes) - Testing multiple nodes simultaneously - Microservices-style robotics architectures **When to use single-process execution:** - All nodes in one file (typical for simple projects) - Projects requiring predictable scheduling across all nodes - Maximum performance with minimal overhead **Important:** Each process runs its own scheduler. Nodes communicate through HORUS shared memory topics, not direct function calls. ### Common Errors | Error | Cause | Fix | |-------|-------|-----| | `No horus.toml found` | Not in a project directory | `cd` into your project or run `horus init` | | `No main source file found` | Missing `src/main.rs` or `src/main.py` | Create the file or specify: `horus run src/your_file.rs` | | `Compilation failed` | Rust/Python syntax errors | Fix the errors shown in the output | | `Permission denied: /dev/shm` | SHM permissions | `sudo chmod 1777 /dev/shm` | | `Address already in use` | Another horus process running | `horus clean --shm` to clear stale SHM | --- ## `horus check` - Validate Project **What it does**: Validates `horus.toml`, source files, and the workspace configuration. **Why it's useful**: Quickly diagnose configuration issues, missing dependencies, or environment problems before building. ### Basic Usage ```bash # Check current directory horus check # Check a specific path horus check path/to/project # Quiet mode (errors only) horus check --quiet ``` ### All Options ```bash horus check [OPTIONS] [PATH] Arguments: [PATH] Path to file, directory, or workspace (default: current directory) Options: -q, --quiet Only show errors, suppress warnings -h, --help Print help ``` ### Examples **Validate before building**: ```bash horus check # Validates horus.toml, build files, and environment ``` **CI/CD validation**: ```bash #!/bin/bash if ! horus check --quiet; then echo "Validation failed" exit 1 fi horus build --release ``` **Related:** - [Static Analysis](/development/static-analysis) — Detailed validation phases and output format - [AI-Assisted Development](/development/ai-assisted-development) — Structured diagnostics for AI coding agents --- ## `horus monitor` - Monitor Everything **What it does**: Opens a visual monitor showing all your running nodes, messages, and performance. **Why it's useful**: Debug problems visually. See message flow in real-time. Monitor performance. ### Basic Usage ```bash # Web monitor (opens in browser) horus monitor # Different port horus monitor 8080 # Text-based (for SSH) horus monitor --tui # Reset monitor password before starting horus monitor --reset-password ``` ### What You See The monitor shows: - **All running nodes** - Names, status, tick rates - **Message flow** - What's talking to what - **Performance** - CPU, memory, latency per node - **Topics** - All active communication channels - **Graph view** - Visual network of your system ### Examples **Start monitoring** (in a second terminal): ```bash # Terminal 1: Run your app horus run --release # Terminal 2: Watch it horus monitor ``` **Access from your phone**: ```bash horus monitor # Visit http://your-computer-ip:3000 from phone ``` **Monitor over SSH**: ```bash ssh robot@192.168.1.100 horus monitor --tui ``` See [Monitor Guide](/development/monitor) for detailed features. --- ## `horus search` - Search Packages **Alias**: `horus s` **What it does**: Search the HORUS registry for available packages and plugins. **Why it's useful**: Find drivers, libraries, and plugins before installing them. ### All Options ```bash horus search [OPTIONS] Arguments: Search query (e.g., "camera", "lidar", "motor") Options: -c, --category Filter by category (camera, lidar, imu, motor, servo, bus, gps, simulation, cli) --json Output as JSON ``` ### Examples **Search for packages**: ```bash horus search lidar ``` **Filter by category**: ```bash horus search driver --category camera ``` **Machine-readable output**: ```bash horus search motor --json ``` --- ## `horus info` - Package/Plugin Info **What it does**: Shows detailed information about a package or plugin, including version, description, dependencies, and metadata. **Why it's useful**: Inspect a package before installing, or check details of an installed package. ### All Options ```bash horus info [OPTIONS] Arguments: Package or plugin name Options: --json Output as JSON ``` ### Examples **Show package details**: ```bash horus info pid-controller ``` **JSON output for tooling**: ```bash horus info rplidar-driver --json ``` --- ## `horus list` - List Installed Packages **What it does**: Lists installed packages and plugins in the current project or globally. **Why it's useful**: See what is installed, verify versions, and audit dependencies. ### All Options ```bash horus list [OPTIONS] Options: -g, --global List global scope packages only -a, --all List all (local + global) --json Output as JSON ``` ### Examples **List local packages**: ```bash horus list ``` **List everything (local + global)**: ```bash horus list --all ``` **List global packages as JSON**: ```bash horus list --global --json ``` --- ## `horus update` - Update Packages **What it does**: Updates project dependencies, the horus CLI tool itself, and installed plugins. **Why it's useful**: Keep dependencies current, apply security patches, and upgrade the CLI without manual reinstallation. ### All Options ```bash horus update [PACKAGE] [OPTIONS] Arguments: [PACKAGE] Specific package to update (updates all deps if omitted) Options: -g, --global Update global scope packages --dry-run Show what would be updated without making changes --self Update the horus CLI tool itself --plugins Update installed plugins to latest versions ``` ### Examples **Update all project dependencies**: ```bash horus update ``` **Update a specific package**: ```bash horus update pid-controller ``` **Update the CLI tool itself**: ```bash horus update --self ``` **Update all plugins**: ```bash horus update --plugins ``` **Preview updates without applying**: ```bash horus update --dry-run ``` **Update global packages**: ```bash horus update --global ``` --- ## `horus publish` - Publish Package **What it does**: Publishes the current package to the HORUS registry. Validates the package, builds it, and uploads to the registry. **Why it's useful**: Share your drivers, libraries, and tools with the community or your team. ### All Options ```bash horus publish [OPTIONS] Options: --dry-run Validate and build the package without actually publishing ``` ### Examples **Publish to registry**: ```bash # First login horus auth login # Then publish from your project directory horus publish ``` **Validate without publishing**: ```bash horus publish --dry-run ``` --- ## `horus unpublish` - Unpublish Package **What it does**: Removes a published package version from the HORUS registry. **Why it's useful**: Retract a broken release or remove a package that should no longer be available. ### All Options ```bash horus unpublish [OPTIONS] Arguments: Package name (supports name@version syntax, e.g. my-pkg@1.0.0) Options: -y, --yes Skip confirmation prompt ``` ### Examples **Unpublish a specific version**: ```bash horus unpublish my-package@1.0.0 ``` **Skip confirmation**: ```bash horus unpublish my-package@1.0.0 --yes ``` --- ## `horus auth signing-key` - Generate Signing Keys **What it does**: Generates an Ed25519 signing key pair for package signing. The private key is stored locally and the public key can be shared or uploaded to the registry. **Why it's useful**: Sign your packages to prove authenticity. Users can verify that packages have not been tampered with. > **Migration**: `horus keygen` still works but is deprecated. Use `horus auth signing-key` instead. ### Basic Usage ```bash horus auth signing-key ``` ### Examples **Generate a new key pair**: ```bash horus auth signing-key # Generates Ed25519 key pair # Private key saved to ~/.horus/keys/ # Public key printed to stdout ``` --- ## `horus auth` - Authentication & Keys **What it does**: Authenticate with the registry, manage API keys, and generate signing keys. **Why it's useful**: Secure access to the package registry for publishing and downloading. ### Commands ```bash # Login with GitHub horus auth login # Generate API key (for CI/CD publishing) horus auth api-key # Generate Ed25519 signing key pair (for package signing) horus auth signing-key # Check who you are horus auth whoami # Logout horus auth logout # Manage API keys horus auth keys list horus auth keys revoke ``` ### Examples **First time setup**: ```bash horus auth login # Opens browser for GitHub login ``` **Check you're logged in**: ```bash horus auth whoami ``` **Generate API key for CI/CD**: ```bash horus auth api-key --name github-actions --environment ci-cd # Save the generated key in your CI secrets ``` **Generate signing key for package integrity**: ```bash horus auth signing-key # Ed25519 key pair saved to ~/.horus/keys/ ``` **Logout**: ```bash horus auth logout ``` --- ## `horus build` - Build Without Running **What it does**: Compiles your project without executing it. **Why it's useful**: Validate compilation, prepare for deployment, or integrate with CI/CD pipelines. ### Basic Usage ```bash # Build current project horus build # Build in release mode horus build --release # Clean build (remove cached artifacts first) horus build --clean ``` ### All Options ```bash horus build [FILES...] [OPTIONS] Options: -r, --release Build in release mode (optimized) -c, --clean Clean before building -q, --quiet Suppress progress indicators -d, --drivers Override detected drivers (comma-separated) -e, --enable Enable capabilities (comma-separated) -h, --help Print help ``` ### Examples **CI/CD build validation**: ```bash horus build --release # Exit code 0 = success, non-zero = failure ``` **Clean release build for deployment**: ```bash horus build --clean --release ``` ### Common Errors | Error | Cause | Fix | |-------|-------|-----| | `No horus.toml found` | Not in a project directory | Run `horus init` or `cd` into project | | `error[E0432]: unresolved import` | Missing dependency | `horus add --source crates.io` | | `.horus/Cargo.toml generation failed` | Malformed horus.toml | `horus check` to validate | | `linker 'cc' not found` | Missing C compiler | `sudo apt install build-essential` | --- ## `horus test` - Run Tests **What it does**: Runs your HORUS project's test suite. **Why it's useful**: Validate functionality, run integration tests with simulation, and ensure code quality. ### Basic Usage ```bash # Run all tests horus test # Run tests matching a filter horus test my_node # Run with parallel execution horus test --parallel # Run simulation tests horus test --sim ``` ### All Options ```bash horus test [OPTIONS] [FILTER] Arguments: [FILTER] Test name filter (runs tests matching this string) Options: -r, --release Run tests in release mode --parallel Allow parallel test execution --sim Enable simulation mode (no hardware required) --integration Run integration tests (tests marked #[ignore]) --nocapture Show test output -j, --test-threads Number of test threads (default: 1) --no-build Skip the build step --no-cleanup Skip shared memory cleanup after tests -v, --verbose Verbose output -d, --drivers Override detected drivers (comma-separated) -e, --enable Enable capabilities (comma-separated) -h, --help Print help ``` ### Examples **Run specific tests**: ```bash horus test sensor_node --nocapture ``` **Fast parallel test run**: ```bash horus test --parallel --release ``` **Integration tests with simulator**: ```bash horus test --integration --sim ``` --- ## `horus clean` - Clean Build Artifacts **What it does**: Removes build artifacts, cached dependencies, and shared memory files. **Why it's useful**: Fix corrupted builds, reclaim disk space, or reset shared memory after crashes. ### Basic Usage ```bash # Clean everything (build + shared memory + cache) horus clean --all # Only clean shared memory horus clean --shm # Preview what would be cleaned horus clean --dry-run ``` ### All Options ```bash horus clean [OPTIONS] Options: --shm Only clean shared memory -a, --all Clean everything (build cache + shared memory + horus cache) -n, --dry-run Show what would be cleaned without removing anything -h, --help Print help -V, --version Print version ``` ### Examples **After a crash (clean stale shared memory)**: ```bash horus clean --shm ``` **Full reset before deployment**: ```bash horus clean --all horus build --release ``` --- ## `horus topic` - Topic Introspection **What it does**: Inspect, monitor, and interact with HORUS topics (shared memory communication channels). **Why it's useful**: Debug message flow, verify data publishing, and measure topic rates. ### Subcommands ```bash horus topic list # List all active topics horus topic echo # Print messages as they arrive horus topic info # Show topic details (type, publishers, subscribers) horus topic hz # Measure publishing rate horus topic pub # Publish a message ``` ### Examples **List all topics**: ```bash horus topic list # Output: # cmd_vel (CmdVel) - 2 publishers, 1 subscriber # scan (LaserScan) - 1 publisher, 3 subscribers # odom (Odometry) - 1 publisher, 1 subscriber ``` **Monitor a topic in real-time**: ```bash horus topic echo scan # Prints each LaserScan message as it arrives ``` **Check publishing rate**: ```bash horus topic hz cmd_vel # Output: average rate: 50.0 Hz, min: 49.2, max: 50.8 ``` **Publish test message**: ```bash horus topic pub cmd_vel '{"stamp_nanos": 0, "linear": 1.0, "angular": 0.5}' ``` --- ## `horus node` - Node Management **What it does**: List, inspect, and control running HORUS nodes. **Why it's useful**: Debug node states, restart misbehaving nodes, or pause nodes during testing. ### Subcommands ```bash horus node list # List all running nodes horus node info # Show detailed node information horus node kill # Terminate a node horus node restart # Restart a node horus node pause # Pause a node's tick execution horus node resume # Resume a paused node ``` ### Examples **List all running nodes**: ```bash horus node list # Output: # NAME PID RATE CPU MEMORY STATUS # SensorNode 12345 100Hz 1.2% 10MB Running # ControllerNode 12346 50Hz 2.5% 15MB Running # LoggerNode 12347 10Hz 0.1% 5MB Paused ``` **Get detailed node info**: ```bash horus node info SensorNode # Shows: tick count, error count, subscribed topics, published topics ``` **Restart a stuck node**: ```bash horus node restart ControllerNode ``` **Pause/resume for debugging**: ```bash horus node pause SensorNode # ... inspect state ... horus node resume SensorNode ``` --- ## `horus service` - Service Interaction **Alias**: `horus srv` **What it does**: Inspect and interact with HORUS services (request/response communication channels). **When to use**: Debug service calls, verify server availability, or test request/response workflows from the command line. ### Subcommands | Subcommand | Description | |-----------|-------------| | `list` | List all active services | | `call ` | Call a service with a JSON request | | `info ` | Show type info and status for a service | | `find ` | Find services matching a name filter | ### All Options ```bash horus service list [-v, --verbose] [--json] horus service call [-t, --timeout ] horus service info horus service find ``` | Subcommand | Flag | Description | |-----------|------|-------------| | `list` | `-v, --verbose` | Show detailed information (servers, clients, topics) | | `list` | `--json` | Output as JSON | | `call` | `-t, --timeout ` | Timeout in seconds (default: 5.0) | ### Examples **List all services**: ```bash horus service list # Output: # NAME SERVERS CLIENTS STATUS # add_two_ints 1 1 active # get_map 1 0 active ``` **Call a service**: ```bash horus service call add_two_ints '{"a": 3, "b": 4}' # Response received: # { "sum": 7 } ``` **Call with custom timeout**: ```bash horus service call get_map '{}' --timeout 10.0 ``` **Show service details**: ```bash horus service info add_two_ints # Name: add_two_ints # Request topic: add_two_ints/request # Response topic: add_two_ints/response # Status: active # Servers: 1 # Clients: 1 ``` **Find services by name**: ```bash horus service find map # get_map # save_map ``` --- ## `horus action` - Action Introspection **Alias**: `horus a` **What it does**: Inspect and interact with long-running action servers. Actions use a goal/feedback/result protocol for tasks that take time to complete. **When to use**: Debug navigation goals, manipulation tasks, or any action-based node. Send goals from the command line and monitor progress. ### Subcommands | Subcommand | Description | |-----------|-------------| | `list` | List all active actions | | `info ` | Show action details (topics, publishers, subscribers) | | `send-goal ` | Send a goal to an action server | | `cancel-goal ` | Cancel an active goal | ### All Options ```bash horus action list [-v, --verbose] [--json] horus action info horus action send-goal [-w, --wait] [-t, --timeout ] horus action cancel-goal [-i, --goal-id ] ``` | Subcommand | Flag | Description | |-----------|------|-------------| | `list` | `-v, --verbose` | Show detailed information (topics, publishers) | | `list` | `--json` | Output as JSON | | `send-goal` | `-w, --wait` | Wait for and display the result | | `send-goal` | `-t, --timeout ` | Timeout when waiting for result (default: 30.0) | | `cancel-goal` | `-i, --goal-id ` | Specific goal ID to cancel (cancels all if omitted) | ### Action Topics Each action `` creates five sub-topics: | Topic | Direction | Purpose | |-------|-----------|---------| | `.goal` | Client -> Server | Clients send goals here | | `.cancel` | Client -> Server | Clients request cancellation | | `.status` | Server -> Client | Server broadcasts goal states | | `.feedback` | Server -> Client | Server sends progress updates | | `.result` | Server -> Client | Server sends final result | ### Examples **List all actions**: ```bash horus action list # Output: # NAME GOALS TOPICS # navigate_to_pose 2 5/5 topics # pick_object 1 3/5 topics ``` **Send a navigation goal**: ```bash horus action send-goal navigate_to_pose '{"target_x": 5.0, "target_y": 3.0}' ``` **Send a goal and wait for the result**: ```bash horus action send-goal navigate_to_pose '{"target_x": 5.0, "target_y": 3.0}' --wait # Sending goal to action: navigate_to_pose # Goal ID: abc12345 # # Goal sent # Feedback: {"progress": 0.5} # Status: Running # Feedback: {"progress": 1.0} # Status: Succeeded # # Result: # { "success": true, "distance_traveled": 5.83 } ``` **Send a goal with custom timeout**: ```bash horus action send-goal navigate_to_pose '{"x": 10.0}' --wait --timeout 60.0 ``` **Cancel a specific goal**: ```bash horus action cancel-goal navigate_to_pose --goal-id abc-123-def ``` **Cancel all goals on an action**: ```bash horus action cancel-goal navigate_to_pose ``` **Get action details**: ```bash horus action info navigate_to_pose # Action Information # Name: navigate_to_pose # Topics: # navigate_to_pose/goal -- clients send goals here # navigate_to_pose/cancel -- clients request cancellation # navigate_to_pose/status -- server broadcasts goal states # navigate_to_pose/feedback -- server sends progress # navigate_to_pose/result -- server sends final result # Goal publishers (clients): 2 # Result subscribers (clients): 1 ``` --- ## `horus log` - View and Filter Logs **What it does**: View, filter, and follow HORUS system logs. **Why it's useful**: Debug issues, monitor specific nodes, and track errors in real-time. ### Basic Usage ```bash # View all recent logs horus log # Filter by node horus log SensorNode # Follow logs in real-time horus log --follow # Show only errors horus log --level error ``` ### All Options ```bash horus log [OPTIONS] [NODE] Arguments: [NODE] Filter by node name Options: -l, --level Filter by log level (trace, debug, info, warn, error) -s, --since Show logs from last duration (e.g., "5m", "1h", "30s") -f, --follow Follow log output in real-time -n, --count Number of recent log entries to show --clear Clear logs instead of viewing --clear-all Clear all logs (including file-based logs) -h, --help Print help ``` ### Examples **Follow logs from a specific node**: ```bash horus log SensorNode --follow ``` **View errors from last 10 minutes**: ```bash horus log --level error --since 10m ``` **Show last 50 warnings and errors**: ```bash horus log --level warn --count 50 ``` --- ## `horus param` - Parameter Management **What it does**: Manage node parameters at runtime (get, set, list, dump, save, load). **Why it's useful**: Tune robot behavior without recompiling, persist configurations, and debug parameter values. ### Subcommands ```bash horus param list # List all parameters horus param get # Get parameter value horus param set # Set parameter value horus param delete # Delete a parameter horus param reset # Reset all parameters to defaults horus param load # Load parameters from YAML file horus param save [-o ] # Save parameters to YAML file horus param dump # Dump all parameters as YAML to stdout ``` ### Examples **List all parameters**: ```bash horus param list # Output: # /SensorNode/sample_rate: 100 # /SensorNode/filter_size: 5 # /ControllerNode/kp: 1.5 # /ControllerNode/ki: 0.1 ``` **Tune a controller at runtime**: ```bash horus param set ControllerNode kp 2.0 horus param set ControllerNode ki 0.2 ``` **Save and restore configuration**: ```bash # Save current params horus param save robot_config.yaml # Later, restore them horus param load robot_config.yaml ``` --- ## `horus tf` - Transform Frames (Coordinate Transforms) **What it does**: Inspect and monitor coordinate frame transforms (similar to ROS tf). **Why it's useful**: Debug transform chains, visualize frame relationships, and verify sensor mounting. ### Subcommands ```bash # Introspection horus tf list # List all frames horus tf echo # Echo transform between two frames horus tf tree # Show frame tree hierarchy horus tf info # Detailed frame information horus tf can # Check if transform is possible horus tf hz # Monitor frame update rates # Recording & Replay horus tf record -o # Record transforms to a .tfr file horus tf play # Replay a .tfr recording horus tf diff # Compare two .tfr recordings # Calibration horus tf tune # Interactively tune a static frame's offset horus tf calibrate # Compute sensor-to-base transform from point pairs (SVD) horus tf hand-eye # Solve hand-eye calibration (AX=XB) from pose pairs ``` ### Examples **View frame tree**: ```bash horus tf tree # Output: # world # └── base_link # ├── laser_frame # ├── camera_frame # └── imu_frame ``` **Monitor a transform**: ```bash horus tf echo laser_frame world # Prints: translation [x, y, z] rotation [qx, qy, qz, qw] ``` **Check transform chain**: ```bash horus tf can laser_frame world # Output: Yes, chain: laser_frame -> base_link -> world ``` ### Recording & Replay Record transforms for offline analysis, comparison, or replay. ```bash # Record transforms for 30 seconds horus tf record -o session1.tfr -d 30 # Replay at half speed horus tf play session1.tfr -s 0.5 # Compare two recordings horus tf diff session1.tfr session2.tfr --threshold-m 0.01 ``` **Options:** | Subcommand | Flag | Description | |------------|------|-------------| | `record` | `-o, --output ` | Output .tfr file path (required) | | `record` | `-d, --duration ` | Maximum recording duration in seconds | | `play` | `-s, --speed ` | Playback speed multiplier (default: 1.0) | | `diff` | `--threshold-m ` | Translation difference threshold in meters (default: 0.001) | | `diff` | `--threshold-deg ` | Rotation difference threshold in degrees (default: 0.1) | | `diff` | `--json` | Output as JSON | ### Calibration Calibrate sensor mounting and hand-eye transforms directly from the CLI. **Tune a static frame interactively:** ```bash # Adjust laser_frame offset with fine steps horus tf tune laser_frame --step-m 0.001 --step-deg 0.1 ``` **Compute sensor-to-base transform from known point correspondences:** ```bash # CSV format: sensor_x,sensor_y,sensor_z,world_x,world_y,world_z horus tf calibrate --points-file calibration_points.csv ``` **Solve hand-eye calibration (AX=XB):** ```bash horus tf hand-eye --robot-poses robot.csv --sensor-poses sensor.csv ``` **Options:** | Subcommand | Flag | Description | |------------|------|-------------| | `tune` | `--step-m ` | Translation step size in meters (default: 0.001) | | `tune` | `--step-deg ` | Rotation step size in degrees (default: 0.1) | | `calibrate` | `--points-file ` | CSV file with point pairs (sensor_x,y,z,world_x,y,z) | | `hand-eye` | `--robot-poses ` | CSV file with robot poses | | `hand-eye` | `--sensor-poses ` | CSV file with sensor poses | --- ## `horus msg` - Message Type Introspection **What it does**: Inspect HORUS message type definitions and schemas. **Why it's useful**: Understand message structures, debug serialization issues, and verify type compatibility. ### Subcommands ```bash horus msg list # List all message types horus msg info # Show message definition horus msg hash # Show hash (for compatibility checking) ``` ### Examples **List available message types**: ```bash horus msg list # Output: # CmdVel (horus_library::messages::cmd_vel) # LaserScan (horus_library::messages::sensor) # Odometry (horus_library::messages::sensor) # Image (horus_library::messages::vision) ``` **Show message definition**: ```bash horus msg info CmdVel # Output: # struct CmdVel { # stamp_nanos: u64, // nanoseconds # linear: f32, // m/s forward velocity # angular: f32, // rad/s turning velocity # } ``` --- ## `horus launch` - Launch Multiple Nodes **What it does**: Launch multiple nodes from a YAML configuration file. **Why it's useful**: Start complex multi-node systems with one command, define node dependencies and parameters. ### Basic Usage ```bash # Launch from file horus launch robot.yaml # Preview without launching horus launch robot.yaml --dry-run # Launch with namespace horus launch robot.yaml --namespace robot1 ``` ### All Options ```bash horus launch [OPTIONS] Arguments: Path to launch file (YAML) Options: -n, --dry-run Show what would launch without actually launching --namespace Namespace prefix for all nodes --list List nodes in the launch file without launching -h, --help Print help ``` ### Launch File Format ```yaml # robot.yaml nodes: - name: sensor_node file: src/sensor.rs rate: 100 params: sample_rate: 100 - name: controller file: src/controller.rs rate: 50 depends_on: [sensor_node] params: kp: 1.5 ki: 0.1 - name: logger file: src/logger.py rate: 10 ``` ### Examples **Launch robot system**: ```bash horus launch robot.yaml ``` **Launch with namespace (for multi-robot)**: ```bash horus launch robot.yaml --namespace robot1 horus launch robot.yaml --namespace robot2 ``` --- ## `horus deploy` - Deploy to Remote Robot(s) **What it does**: Cross-compile and deploy your project to one or more remote robots over SSH. Supports named targets from `deploy.yaml`, fleet deployment to multiple robots, and parallel sync. **Why it's useful**: Deploy from development machine to embedded robots. Build once, sync to many. Supports Raspberry Pi, Jetson, and any Linux target. ### Basic Usage ```bash # Deploy to a host directly horus deploy pi@192.168.1.100 # Deploy to a named target from deploy.yaml horus deploy jetson-01 # Deploy and run immediately horus deploy jetson-01 --run # Deploy to multiple robots horus deploy jetson-01 jetson-02 jetson-03 # Deploy to ALL configured targets horus deploy --all # List configured targets horus deploy --list ``` ### All Options ```bash horus deploy [OPTIONS] [TARGETS]... Arguments: [TARGETS]... Target(s) — named targets from deploy.yaml or direct user@host Options: --all Deploy to ALL targets in deploy.yaml --parallel Deploy to multiple targets in parallel -d, --dir Remote directory (default: ~/horus_deploy) -a, --arch Target architecture (aarch64, armv7, x86_64, native) --run Run the project after deploying --debug Build in debug mode instead of release -p, --port SSH port (default: 22) -i, --identity SSH identity file -n, --dry-run Show what would be done without doing it --list List configured deployment targets ``` ### Fleet Deployment Deploy to multiple robots at once. The build runs once (shared across targets with the same architecture), then syncs to each robot: ```bash # Sequential (default) — deploys one at a time horus deploy jetson-01 jetson-02 jetson-03 # All targets from deploy.yaml horus deploy --all # Dry run to preview fleet deployment horus deploy --all --dry-run ``` ### Configure Named Targets Create `deploy.yaml` in your project root: ```yaml targets: jetson-01: host: nvidia@10.0.0.1 arch: aarch64 dir: ~/robot jetson-02: host: nvidia@10.0.0.2 arch: aarch64 dir: ~/robot arm-controller: host: pi@10.0.0.10 arch: aarch64 dir: ~/arm port: 2222 identity: ~/.ssh/robot_key ``` Then deploy by name: ```bash horus deploy jetson-01 # single target horus deploy jetson-01 jetson-02 # multiple targets horus deploy --all # all targets horus deploy --list # show all configured targets ``` ### Examples **Deploy to Raspberry Pi**: ```bash horus deploy pi@raspberrypi.local --arch aarch64 ``` **Deploy and run on NVIDIA Jetson**: ```bash horus deploy ubuntu@jetson.local --arch aarch64 --run ``` **Deploy entire warehouse fleet**: ```bash horus deploy --all --dry-run # preview first horus deploy --all # deploy to all robots ``` Then deploy with: ```bash horus deploy jetson --run ``` --- ## `horus add` - Add Dependency **What it does**: Adds a dependency to `horus.toml`. Auto-detects the source (crates.io, PyPI, system) based on your project language. Like `cargo add` for the horus ecosystem. **Why it's useful**: Single command to add dependencies from any ecosystem — Rust, Python, system packages, or the horus registry. ### Basic Usage ```bash # Auto-detects source from project language horus add serde # Rust project → crates.io horus add numpy # Python project → PyPI # Explicit source override horus add serde --source crates-io horus add numpy --source pypi horus add libudev --source system horus add horus-nav-stack --source registry # With version and features horus add serde@1.0 --features derive # Add to dev-dependencies horus add criterion --dev # Add as driver horus add camera-driver --driver ``` ### Source Auto-Detection | Project Type | `horus add foo` defaults to | |-------------|----------------------------| | Rust only | crates.io | | Python only | PyPI | | Multi-language | Checks known package tables, then project context | | C++ only | system | Override with `--source`: `crates-io`, `pypi`, `system`, `registry`, `git`, `path` --- ## `horus install` - Install Standalone Package **What it does**: Installs a standalone package or plugin from the horus registry. Like `cargo install` — for tools that aren't project dependencies. **Why it's useful**: Install prebuilt drivers, plugins, and CLI extensions that work across all your projects. ### Basic Usage ```bash horus install horus-sim3d # Install a plugin horus install rplidar-driver@1.2.0 # Install specific version horus install horus-visualizer --plugin # Install as CLI plugin ``` --- ## `horus remove` - Remove Dependency **What it does**: Removes a dependency from `horus.toml`. **Why it's useful**: Clean up unused dependencies from your project. ### Basic Usage ```bash horus remove pid-controller # Remove and purge unused dependencies horus remove sensor-fusion --purge # Remove global package horus remove common-utils --global ``` ### All Options ```bash horus remove [OPTIONS] Arguments: Package/driver/plugin name to remove Options: -g, --global Remove from global scope --purge Also remove unused dependencies -h, --help Print help ``` --- ## `horus plugin` - Plugin Management **Alias**: `horus plugins` **What it does**: Manage HORUS plugins (extensions that add CLI commands or features). Plugins are installed via `horus install --plugin` and managed with the `plugin` subcommands. **Why it's useful**: Enable/disable plugins without uninstalling, verify plugin integrity after updates. ### Subcommands ```bash horus plugin enable # Enable a disabled plugin horus plugin disable # Disable a plugin (keep installed but don't execute) horus plugin verify [plugin] # Verify integrity of installed plugins ``` ### All Options | Subcommand | Flag | Description | |-----------|------|-------------| | `disable` | `--reason ` | Reason for disabling (recorded in config) | | `verify` | `--json` | Output as JSON | ### Examples **Enable a plugin**: ```bash horus plugin enable ros2-bridge ``` **Disable a plugin temporarily**: ```bash horus plugin disable ros2-bridge --reason "debugging" ``` **Verify all plugins**: ```bash horus plugin verify ``` **Verify a specific plugin**: ```bash horus plugin verify ros2-bridge --json ``` **Install and manage a plugin end-to-end**: ```bash horus search visualizer # Find plugins horus install horus-visualizer --plugin # Install as plugin horus plugin disable horus-visualizer # Disable temporarily horus plugin enable horus-visualizer # Re-enable horus plugin verify horus-visualizer # Verify integrity ``` --- ## `horus cache` - Cache Management **What it does**: Manage the HORUS package cache (downloaded packages, compiled artifacts). **Why it's useful**: Reclaim disk space, troubleshoot package issues. ### Subcommands ```bash horus cache info # Show cache statistics (size, package count) horus cache list # List all cached packages horus cache clean # Remove unused packages (--dry-run to preview) horus cache purge # Remove ALL cached packages (-y to skip confirmation) ``` ### Examples **Check cache usage**: ```bash horus cache info # Output: # Location: ~/.horus/cache # Total size: 1.2 GB # Packages: 45 # Last cleaned: 7 days ago ``` **Clean unused packages**: ```bash horus cache clean # Removes packages not used by any project ``` --- ## `horus record` - Record/Replay Management **What it does**: Manage recorded sessions for debugging and testing. **Why it's useful**: Replay exact scenarios, compare runs, debug timing-sensitive issues. ### Subcommands ```bash horus record list # List all recordings horus record info # Show recording details horus record replay # Replay a recording horus record delete # Delete a recording horus record diff # Compare two recordings horus record export # Export to different format horus record inject # Inject recorded data into live scheduler ``` ### Examples **List recordings**: ```bash horus record list # Output: # ID DATE DURATION NODES SIZE # rec_001 2024-01-15 10:30:00 5m 23s 4 45MB # rec_002 2024-01-15 14:15:00 2m 10s 3 18MB ``` **Replay a session**: ```bash horus record replay rec_001 ``` **Compare two runs**: ```bash horus record diff rec_001 rec_002 # Shows differences in timing, message counts, errors ``` **Inject recorded data into live system**: ```bash # Use recorded sensor data with live controller horus record inject rec_001 --nodes SensorNode ``` --- ## `horus blackbox` - BlackBox Flight Recorder **Alias**: `horus bb` **What it does**: Inspects the BlackBox flight recorder for post-mortem crash analysis. The BlackBox automatically records scheduler events, errors, deadline misses, and safety state changes. **Why it's useful**: After a crash or anomaly, review exactly what happened — which nodes failed, when deadlines were missed, and what the safety state was at each tick. ### Basic Usage ```bash # View all recorded events horus blackbox # Show only anomalies (errors, deadline misses, WCET violations, e-stops) horus blackbox --anomalies # Follow mode — stream events in real-time (like tail -f) horus blackbox --follow ``` ### All Options ```bash horus blackbox [OPTIONS] Options: -a, --anomalies Show only anomalies (errors, deadline misses, WCET violations, e-stops) -f, --follow Follow mode — stream new events as they arrive -t, --tick Filter by tick range (e.g. "4500-4510" or "4500") -n, --node Filter by node name (partial, case-insensitive) -e, --event Filter by event type (e.g. "DeadlineMiss", "NodeError") --json Output as machine-readable JSON -l, --last Show only the last N events -p, --path Custom blackbox directory (default: .horus/blackbox/) --clear Clear all blackbox data (with confirmation) ``` ### Examples **View recent anomalies**: ```bash horus bb --anomalies --last 20 ``` **Filter by node and tick range**: ```bash horus bb --node controller --tick 4500-4510 ``` **Stream events in real-time while debugging**: ```bash horus bb --follow --anomalies ``` **Export to JSON for external analysis**: ```bash horus bb --json > blackbox_dump.json ``` **Clear old data**: ```bash horus bb --clear ``` --- ## `horus fmt` - Format Code **What it does**: Formats your project's source code using language-appropriate tools (Rust via `rustfmt`, Python via `ruff`/`black`). **Why it's useful**: Enforce consistent code style across your project without manual formatting. Use `--check` in CI to fail on unformatted code. ### Basic Usage ```bash # Format all code in the project horus fmt # Check formatting without modifying files (useful for CI) horus fmt --check ``` ### All Options ```bash horus fmt [OPTIONS] [-- EXTRA_ARGS] Options: --check Check formatting without modifying files -- Additional arguments passed to underlying tools ``` **Options:** | Flag | Description | |------|-------------| | `--check` | Check formatting without modifying files (exit code 1 if unformatted) | | `-- ` | Additional arguments passed to `rustfmt` or `ruff format` | ### Examples **Format before committing:** ```bash horus fmt git add -A && git commit -m "formatted" ``` **CI formatting check:** ```bash horus fmt --check || (echo "Run 'horus fmt' to fix formatting" && exit 1) ``` **Pass extra arguments to rustfmt:** ```bash horus fmt -- --edition 2021 ``` --- ## `horus lint` - Lint Code **What it does**: Runs linters on your project (Rust via `clippy`, Python via `ruff`/`pylint`). Optionally runs type checking for Python. **Why it's useful**: Catch bugs, anti-patterns, and style issues before they reach production. ### Basic Usage ```bash # Lint all code horus lint # Auto-fix lint issues where possible horus lint --fix # Also run Python type checker (mypy/pyright) horus lint --types ``` ### All Options ```bash horus lint [OPTIONS] [-- EXTRA_ARGS] Options: --fix Auto-fix lint issues where possible --types Also run Python type checker (mypy/pyright) -- Additional arguments passed to underlying tools ``` **Options:** | Flag | Description | |------|-------------| | `--fix` | Auto-fix lint issues where possible | | `--types` | Also run Python type checker (mypy/pyright) | | `-- ` | Additional arguments passed to `clippy` or `ruff check` | ### Examples **Lint and auto-fix:** ```bash horus lint --fix ``` **Full lint with type checking:** ```bash horus lint --types ``` **CI lint gate:** ```bash horus lint || exit 1 ``` --- ## `horus doc` - Generate Documentation **What it does**: Generates documentation for your project. Supports multiple output formats (JSON, Markdown, HTML), doc coverage reporting, API extraction, and topic/message flow graphs. **Why it's useful**: Generate API docs, measure documentation coverage, extract machine-readable API data for tooling, and enforce minimum coverage in CI. ### Basic Usage ```bash # Generate docs and open in browser horus doc --open # Show documentation coverage report horus doc --coverage # Extract machine-readable API documentation as JSON horus doc --extract --json ``` ### All Options ```bash horus doc [OPTIONS] [-- EXTRA_ARGS] Options: --open Open documentation in browser after generating --extract Extract machine-readable API documentation --json Output as JSON --md Output as markdown (for LLM context) --html Output as self-contained HTML report --full Include doc comments in brief output --all Include private/crate-only symbols --lang Filter by language (rust, python) --coverage Show documentation coverage report -o, --output Write output to file instead of stdout --diff Compare against a baseline JSON file --fail-under Fail if doc coverage is below this percentage (for CI) --watch Watch for file changes and regenerate -- Additional arguments passed to underlying tools ``` **Options:** | Flag | Description | |------|-------------| | `--open` | Open documentation in browser after generating | | `--extract` | Extract machine-readable API documentation | | `--json` | Output as JSON | | `--md` | Output as markdown (useful for LLM context) | | `--html` | Output as self-contained HTML report | | `--full` | Include doc comments in brief output | | `--all` | Include private/crate-only symbols | | `--lang ` | Filter by language (`rust`, `python`) | | `--coverage` | Show documentation coverage report | | `-o, --output ` | Write output to file instead of stdout | | `--diff ` | Compare against a baseline JSON file | | `--fail-under ` | Fail if doc coverage is below this percentage (for CI) | | `--watch` | Watch for file changes and regenerate | ### Examples **Generate and browse docs:** ```bash horus doc --open ``` **CI documentation coverage gate:** ```bash horus doc --coverage --fail-under 80 ``` **Extract API docs as markdown for LLM context:** ```bash horus doc --extract --md -o api.md ``` **Track API changes between releases:** ```bash horus doc --extract --json -o api-v2.json horus doc --diff api-v1.json ``` **Watch mode during development:** ```bash horus doc --watch --open ``` --- ## `horus bench` - Run Benchmarks **What it does**: Runs benchmarks for your HORUS project. Supports filtering by name and passing extra arguments to the underlying benchmark framework. **Why it's useful**: Measure and track performance of your nodes, algorithms, and IPC throughput. ### Basic Usage ```bash # Run all benchmarks horus bench # Run benchmarks matching a filter horus bench latency ``` ### All Options ```bash horus bench [OPTIONS] [FILTER] [-- EXTRA_ARGS] Arguments: [FILTER] Filter benchmarks by name Options: -- Additional arguments passed to underlying tools ``` **Options:** | Flag | Description | |------|-------------| | `[FILTER]` | Filter benchmarks by name (substring match) | | `-- ` | Additional arguments passed to the benchmark runner | ### Examples **Run all benchmarks:** ```bash horus bench ``` **Run specific benchmarks:** ```bash horus bench ipc_throughput ``` **Pass extra arguments to criterion:** ```bash horus bench -- --sample-size 100 ``` --- ## `horus deps` - Dependency Insight **What it does**: Inspect, audit, and manage your project's dependencies. Provides subcommands for viewing the dependency tree, explaining why a package is included, checking for outdated packages, and running security audits. **Why it's useful**: Understand your dependency graph, find unused or outdated packages, and catch known vulnerabilities. ### Subcommands ```bash horus deps tree # Show dependency tree horus deps why # Explain why a dependency is included horus deps outdated # Check for outdated dependencies horus deps audit # Security audit of dependencies ``` ### Examples **View dependency tree:** ```bash horus deps tree ``` **Find out why a package is included:** ```bash horus deps why serde # Output: serde is required by: # horus_core -> serde (serialize node configs) # horus_library -> serde (message serialization) ``` **Check for outdated dependencies:** ```bash horus deps outdated ``` **Run security audit:** ```bash horus deps audit # Checks against RustSec advisory database ``` **Pass extra arguments to underlying tools:** ```bash horus deps tree -- --depth 2 ``` --- ## `horus doctor` - Health Check **What it does**: Runs a comprehensive health check of your HORUS ecosystem, including toolchain versions, configuration validity, system dependencies, and environment setup. **Why it's useful**: Quickly diagnose setup issues, verify that all required tools are installed, and ensure your environment is ready for development. ### Basic Usage ```bash # Run health check horus doctor # Verbose output with detailed check results horus doctor --verbose # Output as JSON (for tooling) horus doctor --json ``` ### All Options ```bash horus doctor [OPTIONS] Options: -v, --verbose Show detailed output for each check --json Output as JSON ``` **Options:** | Flag | Description | |------|-------------| | `-v, --verbose` | Show detailed output for each check | | `--json` | Output as JSON | ### Examples **Quick health check:** ```bash horus doctor # Output: # [OK] Rust toolchain (1.78.0) # [OK] Python 3.12 # [OK] horus.toml valid # [OK] Shared memory accessible # [OK] Drivers: 3 driver(s) reachable # [WARN] clippy not installed (run: rustup component add clippy) ``` **Driver reachability** (checks `[drivers]` entries from horus.toml): ```bash horus doctor --verbose # ... # ✓ Drivers: 3 driver(s) checked, some unreachable # ✓ driver 'imu': /dev/i2c-1 found # ✗ driver 'arm': /dev/ttyUSB0 not found # ! driver 'lidar': 192.168.1.201:2368 unreachable ``` Checks serial ports (`Path::exists`), I2C buses (`/dev/i2c-N`), and network endpoints (`TcpStream` with 2s timeout). No terra dependency — pure OS-level checks. **Verbose diagnostics:** ```bash horus doctor --verbose ``` **CI environment validation:** ```bash horus doctor --json | jq '.checks[] | select(.status != "ok")' ``` --- ## `horus self update` - Update CLI **What it does**: Updates the horus CLI binary to the latest version. Like `rustup self update`. **Why it's useful**: Keep your CLI current without affecting project dependencies (use `horus update` for deps). > **Migration**: `horus upgrade` still works but is deprecated. Use `horus self update` instead. ### Basic Usage ```bash # Update the CLI binary horus self update # Check for updates without installing horus self update --check ``` ### Options | Flag | Description | |------|-------------| | `--check` | Check for available updates without installing | ### Examples **Check if update is available:** ```bash horus self update --check # Output: Current: 0.1.9, Latest: 0.2.0 — update available ``` **Update the CLI:** ```bash horus self update ``` **Update project dependencies separately:** ```bash horus update # Update deps in horus.toml horus self update # Update CLI binary ``` --- ## `horus config` - Config Management **What it does**: View and edit `horus.toml` settings from the command line using dot-notation keys. **Why it's useful**: Quickly inspect or modify project configuration without opening an editor. Useful for scripting and CI. ### Subcommands ```bash horus config get # Get a config value horus config set # Set a config value horus config list # List all config values ``` ### Examples **Get a config value:** ```bash horus config get package.name # Output: my_robot ``` **Set a config value:** ```bash horus config set package.version "0.2.0" ``` **List all config values:** ```bash horus config list # Output: # package.name = "my_robot" # package.version = "0.1.0" # package.language = "rust" ``` --- ## `horus migrate` - Migrate to horus.toml **What it does**: Migrates an existing project to the unified `horus.toml` format. Detects existing `Cargo.toml`, `pyproject.toml`, or `package.xml` files and consolidates them into a single `horus.toml` manifest. **Why it's useful**: Move legacy projects to the unified HORUS manifest format. The `--dry-run` flag lets you preview changes before committing. ### Basic Usage ```bash # Migrate (interactive, asks for confirmation) horus migrate # Preview what would change horus migrate --dry-run # Skip confirmation prompts horus migrate --force ``` ### All Options ```bash horus migrate [OPTIONS] Options: -n, --dry-run Show what would change without modifying -f, --force Skip confirmation prompts ``` **Options:** | Flag | Description | |------|-------------| | `-n, --dry-run` | Show what would change without modifying | | `-f, --force` | Skip confirmation prompts | ### Examples **Preview migration:** ```bash horus migrate --dry-run # Output: # Would create: horus.toml # Would move: Cargo.toml -> .horus/Cargo.toml # Would extract: 5 dependencies from Cargo.toml ``` **Migrate existing Rust project:** ```bash cd my-existing-project horus migrate --force # horus.toml created, Cargo.toml moved to .horus/ ``` --- ## `horus scripts` - Run Scripts **Alias**: `horus script` **What it does**: Runs a named script defined in the `[scripts]` section of `horus.toml`. If no script name is given, lists all available scripts. **Why it's useful**: Define project-specific commands (test suites, deployment steps, data processing) in `horus.toml` and run them with a single command. ### Basic Usage ```bash # List available scripts horus scripts # Run a script by name horus scripts deploy # Run a script with arguments horus scripts test -- --verbose ``` ### All Options ```bash horus scripts [NAME] [-- ARGS] Arguments: [NAME] Script name to run (omit to list available scripts) -- Arguments to pass to the script ``` ### Examples **Define scripts in horus.toml:** ```toml [scripts] deploy = "rsync -avz ./target/release/ robot@192.168.1.100:~/app/" test-hw = "horus test --integration --sim" bench = "horus bench -- --sample-size 50" ``` **List scripts:** ```bash horus scripts # Output: # deploy rsync -avz ./target/release/ robot@192.168.1.100:~/app/ # test-hw horus test --integration --sim # bench horus bench -- --sample-size 50 ``` **Run a script:** ```bash horus scripts deploy ``` --- ## `horus completion` - Shell Completions **What it does**: Generates shell completion scripts for bash, zsh, fish, elvish, or PowerShell. This is a hidden command (not shown in `horus --help`). **Why it's useful**: Get tab-completion for all horus commands and flags in your shell. ### Basic Usage ```bash # Bash horus completion bash > ~/.local/share/bash-completion/completions/horus # Zsh horus completion zsh > ~/.zfunc/_horus # Fish horus completion fish > ~/.config/fish/completions/horus.fish ``` ### Supported Shells | Shell | Value | |-------|-------| | Bash | `bash` | | Zsh | `zsh` | | Fish | `fish` | | Elvish | `elvish` | | PowerShell | `powershell` | --- ## Common Workflows ### First Time Using HORUS ```bash # Create a project horus new my_robot --rust cd my_robot # Run it horus run # Monitor it (open a second terminal) horus monitor ``` ### Daily Development Cycle ```bash # Edit your code vim src/main.rs # Format + lint before running horus fmt horus lint # Run and test horus run horus test # Monitor in another terminal horus monitor ``` ### Debugging a Motor Stutter Your robot's motor is stuttering. Here's how to diagnose with the CLI: ```bash # Step 1: Run with monitoring horus run --release # Step 2: Watch the motor command topic (another terminal) horus topic echo motor/cmd_vel # Step 3: Check if the motor node is missing deadlines horus node info motor_ctrl # Look at: avg_tick_ms, max_tick_ms, deadline_misses # Step 4: Check the blackbox for deadline miss events horus blackbox --follow # Step 5: Record a session for offline analysis horus record start # ... reproduce the stutter ... horus record stop # Step 6: Replay the session to reproduce horus record replay --session latest ``` ### Adding a New Sensor You're adding a LiDAR to your robot: ```bash # Step 1: Check if there's a driver package available horus search rplidar horus install rplidar # Step 2: If not, create your own driver horus new lidar_driver --rust cd lidar_driver # ... write your driver code ... # Step 3: Run and verify data flow horus run # In another terminal: horus topic list # See all topics horus topic echo lidar/scan # Watch scan data horus topic hz lidar/scan # Check publish rate # Step 4: Verify coordinate frames horus tf tree # See frame hierarchy horus tf echo lidar base_link # Check transform ``` ### Preparing for Field Deployment Pre-deployment checklist using the CLI: ```bash # Step 1: Validate everything horus doctor # Ecosystem health check horus check # Validate horus.toml horus fmt --check # Ensure code is formatted horus lint # Check for issues # Step 2: Run tests horus test horus bench # Check performance hasn't regressed # Step 3: Check dependencies horus deps outdated # Find outdated deps horus deps audit # Security audit # Step 4: Clean build and verify horus run --clean --release # Step 5: Deploy to robot horus deploy robot@192.168.1.10 --release ``` ### Multi-Robot Development Working with multiple robots that share code: ```bash # Step 1: Publish shared packages cd common_messages horus publish cd ../lidar_driver horus publish # Step 2: Install on each robot project cd robot_alpha horus install common_messages lidar_driver cd ../robot_beta horus install common_messages lidar_driver # Step 3: Check running nodes horus node list ``` ### CI/CD Pipeline ```bash # In your CI config (GitHub Actions, GitLab CI, etc.): horus fmt --check # Fail if unformatted horus lint # Fail on lint errors horus check # Validate project horus test # Run all tests horus bench --fail-under 0.95 # Performance gate (optional) horus doc --coverage --fail-under 80 # Doc coverage gate (optional) ``` ### Share Your Work ```bash # Login once horus auth login # Publish horus publish # Others can now: horus install your-package-name ``` --- ## Troubleshooting ### "command not found: horus" Add cargo to your PATH: ```bash export PATH="$HOME/.cargo/bin:$PATH" echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc source ~/.bashrc ``` ### "Port already in use" ```bash # Use different port horus monitor 3001 # Or kill the old process lsof -ti:3000 | xargs kill -9 ``` ### Build is slow First build is always slow (5-10 min). After that it's fast (seconds). Use `--release` only when you need speed, not during development. ### "Failed to create Topic" Topic name conflict. Try a unique name or clean up stale shared memory. **Note**: HORUS automatically cleans up shared memory after each run using session isolation. This error usually means a previous run crashed. ```bash # Clean all HORUS shared memory (if needed after crashes) horus clean --shm ``` --- ## Environment Variables Optional configuration: ```bash # Custom registry (for companies) export HORUS_REGISTRY_URL=https://your-company-registry.com # Debug mode (see what's happening) export RUST_LOG=debug horus run # CI/CD authentication export HORUS_API_KEY=your-key-here ``` --- ## Utility Scripts Beyond the `horus` CLI, the repository includes helpful scripts: ```bash ./install.sh # Install or update HORUS ``` See **[Troubleshooting & Maintenance](/troubleshooting)** for complete details. --- ## Next Steps Now that you know the commands: 1. **[Quick Start](/getting-started/quick-start)** - Build your first app 2. **[node! Macro](/concepts/node-macro)** - Write less code 3. **[Monitor Guide](/development/monitor)** - Master monitoring 4. **[Examples](/rust/examples/basic-examples)** - See real applications **Having issues?** Check the **[Troubleshooting Guide](/troubleshooting)** for solutions to common problems. --- ## AI Integration Path: /development/ai-integration Description: Integrate AI and ML models into HORUS robotics applications # AI Integration HORUS's sub-microsecond IPC makes it well-suited for combining real-time control with AI inference. This guide covers patterns for integrating ML models into HORUS applications. ## Overview HORUS supports AI integration through two main approaches: **Python ML Nodes** (Recommended for Prototyping) - Use any Python ML library (PyTorch, TensorFlow, ONNX, etc.) - Hardware nodes handle camera/sensor capture - Pub/sub connects ML pipeline to control nodes - 10-100ms typical inference latency **Rust Inference** (For Production) - ONNX Runtime via `ort` crate - Tract (pure Rust inference engine) - 1-50ms typical inference latency ### Architecture Pattern Sensor Node
~1ms"] -->|Topic| M["ML Node
~10-50ms"] M -->|Topic| C["Control Node
~1μs"] style S fill:#3b82f6,color:#fff style M fill:#8b5cf6,color:#fff style C fill:#10b981,color:#fff `} caption="AI pipeline: Sensor → ML inference → Real-time control" /> The key insight: keep AI inference in dedicated nodes. HORUS topics decouple the fast control loop from slower ML processing, so a slow inference step doesn't block motor commands. --- ## Python ML Integration The fastest way to add AI to a HORUS application is through Python nodes. Python has the richest ML ecosystem, and HORUS's Python bindings give you full access to the pub/sub system. ### Camera + ML Pipeline ```python from horus import Node, Scheduler import numpy as np # Simulated camera node (replace with your camera capture logic) def camera_tick(node): # In a real robot, capture from camera hardware here frame = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) node.send("cam.image_raw", frame.tolist()) cam = Node(name="camera", pubs=["cam.image_raw"], tick=camera_tick, rate=30, order=0) # ML processing node def ml_tick(node): if node.has_msg("cam.image_raw"): frame = node.recv("cam.image_raw") # Run your ML model here # e.g., model.predict(frame), torch inference, etc. result = process_frame(frame) node.send("detections", result) ml_node = Node( name="ml_processor", subs=["cam.image_raw"], pubs=["detections"], tick=ml_tick, rate=10, # Process at 10 FPS order=1 ) # Control node reacts to detections def control_tick(node): if node.has_msg("detections"): detections = node.recv("detections") # React to ML output if detections.get("obstacle_detected"): node.send("cmd_vel", {"linear": 0.0, "angular": 0.5}) else: node.send("cmd_vel", {"linear": 1.0, "angular": 0.0}) controller = Node( name="controller", subs=["detections"], pubs=["cmd_vel"], tick=control_tick, rate=30, order=2 ) scheduler = Scheduler() scheduler.add(cam) scheduler.add(ml_node) scheduler.add(controller) scheduler.run() ``` ### PyTorch Example ```python from horus import Node, run import torch # Load model once at startup model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.eval() def detect_tick(node): if node.has_msg("cam.image_raw"): frame = node.recv("cam.image_raw") results = model(frame) detections = results.pandas().xyxy[0].to_dict('records') node.send("detections", detections) node = Node( name="yolo_detector", subs=["cam.image_raw"], pubs=["detections"], tick=detect_tick, rate=10 ) run(node) ``` ### ONNX Runtime (Python) ```python from horus import Node, run import onnxruntime as ort import numpy as np session = ort.InferenceSession("model.onnx") input_name = session.get_inputs()[0].name def inference_tick(node): if node.has_msg("cam.image_raw"): frame = node.recv("cam.image_raw") # Preprocess input_data = np.array(frame).astype(np.float32) input_data = np.expand_dims(input_data, axis=0) # Run inference outputs = session.run(None, {input_name: input_data}) node.send("ml.output", outputs[0].tolist()) node = Node( name="onnx_inference", subs=["cam.image_raw"], pubs=["ml.output"], tick=inference_tick, rate=15 ) run(node) ``` ### ML Utilities HORUS provides Python ML utilities for common patterns: ```python from horus.ml_utils import ONNXInferenceNode, PerformanceMonitor # Pre-built ONNX inference node class MyDetector(ONNXInferenceNode): def __init__(self): super().__init__( model_path="models/detector.onnx", input_topic="cam.image_raw", output_topic="detections" ) def preprocess(self, frame): # Resize, normalize, etc. return processed_frame def postprocess(self, output): # Parse model output into detections return detections # Performance monitoring monitor = PerformanceMonitor(window_size=100) monitor.record(12.5) # Record inference time in ms stats = monitor.get_stats() print(f"Avg: {stats['avg_latency_ms']:.1f}ms, P95: {stats['p95_latency_ms']:.1f}ms, FPS: {stats['fps']:.0f}") ``` See [ML Utilities](/python/library/ml-utilities) for the full API. --- ## Rust Inference For production deployments where you need maximum performance, integrate ML inference directly in Rust. ### ONNX Runtime (ort crate) The `ort` crate provides Rust bindings for ONNX Runtime: Add to your `Cargo.toml`: ```toml [dependencies] horus = { path = "..." } horus_library = { path = "..." } ort = "2.0" ndarray = "0.15" ``` ```rust use horus::prelude::*; use ort::{GraphOptimizationLevel, Session}; use ndarray::Array; struct InferenceNode { session: Session, input_name: String, } impl InferenceNode { fn new(model_path: &str) -> Result> { let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? .commit_from_file(model_path)?; let input_name = session.inputs[0].name.clone(); Ok(Self { session, input_name }) } fn infer(&self, input: &[f32]) -> Option> { let input_array = Array::from_shape_vec((1, input.len()), input.to_vec()).ok()?; let outputs = self.session.run( ort::inputs![&self.input_name => input_array.view()].ok()? ).ok()?; let output = outputs[0].try_extract_tensor::().ok()?; Some(output.view().iter().copied().collect()) } } ``` ### Tract (Pure Rust) Tract runs ONNX models with zero external dependencies: Add to your `Cargo.toml`: ```toml [dependencies] horus = { path = "..." } horus_library = { path = "..." } tract-onnx = "0.21" ``` ```rust use tract_onnx::prelude::*; fn load_model(path: &str) -> TractResult, Graph>>> { tract_onnx::onnx() .model_for_path(path)? .into_optimized()? .into_runnable() } fn run_inference(model: &SimplePlan, Graph>>, input: &[f32]) -> Option> { let input_tensor = tract_ndarray::arr1(input).into_dyn(); let result = model.run(tvec!(input_tensor.into())).ok()?; let output = result[0].to_array_view::().ok()?; Some(output.iter().copied().collect()) } ``` ### Model Format Comparison | Format | Crate | Use Case | External Deps | |--------|-------|----------|---------------| | **ONNX** | `ort` | General (PyTorch, TF exports) | ONNX Runtime C lib | | **ONNX** | `tract-onnx` | Pure Rust inference | None | | **TFLite** | `tflite` | Edge/mobile models | TFLite C lib | --- ## Cloud API Integration For complex reasoning tasks (task planning, scene understanding, natural language), call cloud APIs from HORUS nodes. ### Python (Recommended) ```python from horus import Node, run import requests import os API_KEY = os.environ["OPENAI_API_KEY"] def planner_tick(node): if node.has_msg("user.goal"): goal = node.recv("user.goal") response = requests.post( "https://api.openai.com/v1/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json={ "model": "gpt-4", "messages": [ {"role": "system", "content": "Generate robot action plans as JSON."}, {"role": "user", "content": goal} ], "max_tokens": 500 } ) plan = response.json()["choices"][0]["message"]["content"] node.send("robot.plan", plan) node = Node( name="planner", subs=["user.goal"], pubs=["robot.plan"], tick=planner_tick, rate=1 # Check for goals once per second ) run(node) ``` ### Rust (reqwest) ```rust use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; #[derive(Serialize)] struct ChatRequest { model: String, messages: Vec, max_tokens: u32, } #[derive(Serialize, Deserialize)] struct ChatMessage { role: String, content: String, } #[derive(Deserialize)] struct ChatResponse { choices: Vec, } #[derive(Deserialize)] struct ChatChoice { message: ChatMessage, } fn call_llm(client: &Client, api_key: &str, prompt: &str) -> Option { let request = ChatRequest { model: "gpt-4".to_string(), messages: vec![ChatMessage { role: "user".to_string(), content: prompt.to_string(), }], max_tokens: 500, }; let response = client .post("https://api.openai.com/v1/chat/completions") .header("Authorization", format!("Bearer {}", api_key)) .json(&request) .send() .ok()?; let chat_response: ChatResponse = response.json().ok()?; Some(chat_response.choices[0].message.content.clone()) } ``` --- ## Performance Considerations ### Latency Budget Typical robotics control loop at 100Hz (10ms cycle): ``` Sensor capture: ~1-16ms (hardware dependent) ML inference: ~5-50ms (model dependent) Topic transfer: ~85ns (HORUS shared memory) Control logic: ~1μs (HORUS node tick) Motor command: ~1ms (hardware actuator) ``` ML inference is typically the bottleneck. Strategies to manage this: ### Throttle Inference Process every Nth frame instead of every frame: ```python frame_count = 0 def ml_tick(node): global frame_count if node.has_msg("cam.image_raw"): frame_count += 1 if frame_count % 5 == 0: # Every 5th frame frame = node.recv("cam.image_raw") result = model.predict(frame) node.send("detections", result) ``` ### Async Processing Run ML in a background thread so the control loop isn't blocked: ```python import horus import asyncio async def async_ml_tick(node): if node.has_msg("cam.image_raw"): frame = node.recv("cam.image_raw") # Run in thread pool to avoid blocking loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, model.predict, frame) node.send("detections", result) # Async is auto-detected from the async tick function ml_node = horus.Node( name="async_ml", subs=["cam.image_raw"], pubs=["detections"], tick=async_ml_tick, rate=10 ) ``` ### Use Appropriate Models | Task | CPU Model | GPU Model | Cloud API | |------|-----------|-----------|-----------| | **Object Detection** | YOLOv8n (ONNX) | YOLOv8x | GPT-4 Vision | | **Classification** | MobileNet (TFLite) | EfficientNet | Cloud Vision | | **Pose Estimation** | MediaPipe | OpenPose | - | | **Task Planning** | Phi-3 Mini | Llama 3 | GPT-4 / Claude | | **Depth Estimation** | MiDaS Small | MiDaS Large | - | --- ## Best Practices 1. **Separate concerns**: Keep AI inference in dedicated nodes. Don't mix ML code with control logic. 2. **Handle failures gracefully**: AI inference can fail. Always have a safe fallback: ```python def control_tick(node): if node.has_msg("detections"): detections = node.recv("detections") react_to(detections) else: # Safe default when no detections available node.send("cmd_vel", {"linear": 0.0, "angular": 0.0}) ``` 3. **Monitor performance**: Use `horus monitor` to watch node timing and message flow: ```bash horus monitor # See which nodes are slow ``` 4. **Start with Python**: Prototype in Python first, then move performance-critical inference to Rust if needed. 5. **Cache results**: For cloud APIs, cache common responses to reduce latency and cost. --- ## See Also - [Python Examples](/python/examples) - Complete example applications - [Message Library](/python/library/python-message-library) - Available message types - [Python Bindings](/python/api/python-bindings) - Core Python API --- ## AI-Assisted Development Path: /development/ai-assisted-development Description: Use HORUS with AI coding agents — structured errors, machine-readable API extraction, and auto-fix workflows # AI-Assisted Development HORUS is built for vibe coding. Every error includes a fix command. Every API is extractable as JSON. AI agents can understand, build, test, and fix your robotics project without human intervention. --- ## The AI Development Loop An AI agent working on a HORUS project follows this loop: ``` 1. Understand the project horus doc --extract --json 2. Build and check horus build --json-diagnostics 3. Parse errors (structured JSON, one per line) 4. Auto-fix Execute the "fix" command from each diagnostic 5. Run tests horus test 6. Check what changed horus doc --extract --diff baseline.json 7. Repeat ``` Every step produces machine-readable output. No regex parsing of compiler errors needed. --- ## Structured Error Diagnostics ### Enable JSON diagnostics ```bash horus build --json-diagnostics horus run --json-diagnostics ``` Every error, warning, and hint is emitted as a JSON object on stderr: ```json {"tool":"cargo","code":"H001","severity":"error","message":"Crate 'serde' not found on crates.io","hint":"Check the name or add it with:\n horus add serde","fix":{"type":"command","command":"horus add serde"},"docs_url":"https://horus.dev/errors/missing-crate"} ``` ### The Fix Field Every diagnostic includes a `fix` field that the AI agent can execute directly: ```json { "fix": { "type": "command", "command": "horus add serde" } } ``` The agent parses this, runs `horus add serde`, and the dependency is installed. No guessing. ### Error Code Catalog Diagnostics use standardized codes (H001-H064) grouped by tool: | Range | Tool | Examples | |-------|------|---------| | H001-H007 | Cargo (Rust) | Missing crate, version conflict, linker error | | H010-H014 | Pip (Python) | Package not found, version conflict, wheel build failure | | H030-H040 | Runtime | ModuleNotFoundError, ImportError, SyntaxError | | H050-H064 | Preflight | Missing toolchain, low disk space | ### JSON Output for Build/Test Results ```bash # Build with JSON result horus build --json # Output: {"success": true, "command": "build"} # Or: {"success": false, "command": "build", "errors": [{"message": "..."}]} # Test with JSON result horus test --json # Output: {"success": true, "command": "test"} ``` --- ## API Extraction for Context AI agents need to understand the project's API surface before making changes. `horus doc --extract` provides this in a single command. ### Quick Overview ```bash # Brief text summary (fits in any context window) horus doc --extract ``` Output: ``` # my_robot v0.1.0 — 24 symbols, 78% documented # 3 nodes, 4 messages, 5 topics ## Message Types CmdVel { linear: f32, angular: f32 } Odometry { x: f64, y: f64, theta: f64 } ## Nodes ControllerNode (impl Node) [100hz, Rt] pub -> cmd_vel: CmdVel sub <- odom: Odometry SensorNode (impl Node) [200hz, Rt] pub -> imu: ImuReading ## Topic Graph cmd_vel: CmdVel ControllerNode -> MotorDriver odom: Odometry MotorDriver -> ControllerNode ## src/controller.rs — PID controller struct ControllerNode { pid: PidState } impl Node fn new(kp: f64, ki: f64, kd: f64) -> Result ``` ### Full JSON for Programmatic Access ```bash horus doc --extract --json ``` Returns a `ProjectDoc` with: - All symbols (functions, structs, enums, traits, messages, services, actions) - Doc comments and deprecation annotations - Trait implementations and method associations - Message flow graph (which nodes publish/subscribe to which topics) - Entry points (main functions, Node implementations) - Documentation coverage statistics ### Markdown for LLM System Prompts ```bash horus doc --extract --md > api.md ``` Produces markdown suitable for injecting into an LLM's context window. Include in your `CLAUDE.md` or system prompt: ```markdown # Project API Reference Run `horus doc --extract --md` for current API surface. ``` ### Write to File ```bash horus doc --extract --json -o api.json horus doc --extract --html -o docs/api.html ``` ### Filter by Language ```bash horus doc --extract --lang rust # Only Rust symbols horus doc --extract --lang python # Only Python symbols ``` --- ## API Diff for Change Detection Compare the current API against a saved baseline to detect breaking changes: ```bash # Save baseline (e.g., on main branch) horus doc --extract --json -o baseline.json # After changes, diff against baseline horus doc --extract --diff baseline.json ``` Output: ``` API Changes: Added: + src/sensors/lidar.rs: function pub fn calibrate(&mut self) Removed: [!] BREAKING - src/legacy.rs: function pub fn old_handler() Changed: ~ src/controller.rs: function compute was: pub fn compute(&mut self, setpoint: f64, measurement: f64) -> f64 now: pub fn compute(&mut self, setpoint: f64, measurement: f64, dt: f64) -> f64 [!] BREAKING: added parameter `dt: f64` Summary: +1 added, -1 removed, ~1 changed, 1 breaking changes ``` ### CI Integration ```bash # Fail CI if breaking changes detected horus doc --extract --diff baseline.json # Exit code 1 if breaking changes found, 0 otherwise ``` ### Documentation Coverage Gate ```bash # Fail if coverage drops below 60% horus doc --extract --coverage --fail-under 60 ``` --- ## Watch Mode for Live Development ```bash # Regenerate docs on every file save horus doc --extract --watch # Write to file on every change horus doc --extract --json --watch -o api.json ``` The agent can poll `api.json` to stay updated as the code changes. --- ## Self-Contained HTML Report ```bash horus doc --extract --html -o api-docs.html ``` Generates a single HTML file with: - Embedded CSS (dark mode support) - Client-side search - Collapsible module sections - SVG topic flow graph - Coverage table - TODO/FIXME list No external dependencies — works offline, safe to share. --- ## Typical Agent Workflow ### Step 1: Understand the project ```bash horus doc --extract --json -o api.json # Agent reads api.json, understands: # - What nodes exist and their topics # - What message types are defined # - What the public API surface looks like ``` ### Step 2: Make changes The agent edits source files based on the API understanding. ### Step 3: Build and auto-fix ```bash horus build --json-diagnostics 2> errors.jsonl # Agent parses each line, extracts "fix" commands, executes them # Example: {"fix": {"type": "command", "command": "horus add tokio"}} # Agent runs: horus add tokio # Rebuild until no errors ``` ### Step 4: Verify ```bash horus test --json horus doc --extract --diff baseline.json --json # Agent checks: tests pass? Any breaking changes? ``` ### Step 5: Report ```bash horus doc --extract --coverage # Agent reports: documentation coverage, new symbols, changes ``` --- ## CLAUDE.md Integration Add to your project's `CLAUDE.md` for AI agents: ```markdown ## Build Commands horus build --json-diagnostics # Build with structured errors horus test --json # Run tests horus doc --extract --json # Get API surface ## Auto-Fix Workflow When build fails, parse stderr JSON lines. Each diagnostic has a "fix" field with a command to run. Execute fix commands, then rebuild. ## API Understanding Run `horus doc --extract --brief` to see the project API. The topic graph shows message flow between nodes. ``` --- ## See Also - [CLI Reference](/development/cli-reference) — Full command documentation - [Testing Guide](/development/testing) — Writing and running tests - [Error Handling](/development/error-handling) — Error codes and diagnostics --- ## Debugging Workflows Path: /development/debugging Description: Step-by-step debugging for deadline misses, panics, and performance bottlenecks # Debugging Workflows Three concrete workflows for the most common issues: deadline misses, panics, and performance problems. ## Workflow 1: "My Motor Stutters" Stuttering usually means deadline misses — the control loop is not completing within its budget. ### Step 1: Check Scheduler Output Enable monitoring and look for deadline miss warnings in stderr: ```rust let mut scheduler = Scheduler::new() .verbose(true) .tick_rate(1000_u64.hz()); ``` The scheduler prints a timing report on shutdown. Look for lines like: ``` [WARN] motor_ctrl: 12 deadline misses (worst: 2.3ms, budget: 1.0ms) ``` ### Step 2: Profile Tick Timing Use `profile()` to get percentile statistics: ```rust let report = scheduler.profile(5000)?; println!("{report}"); // Check per-node budget utilization for node in &report.nodes { if let Some(used) = node.budget_used { if used > 0.8 { println!("WARNING: {} using {:.0}% of budget", node.name, used * 100.0); } } } ``` If `p99` exceeds the budget, the node has latency spikes. If `p99` is much higher than `median`, the node's execution time is inconsistent. ### Step 3: Use Blackbox to Find the Exact Tick Enable the blackbox to record the last N ticks per node: ```rust let mut scheduler = Scheduler::new() .verbose(true) .with_blackbox(64) .tick_rate(1000_u64.hz()); ``` After a miss, inspect the blackbox to find what happened on the tick that exceeded the budget. The blackbox records tick duration, input values, and events. ### Step 4: Fix Common Causes | Cause | Symptom | Fix | |-------|---------|-----| | Allocation in `tick()` | Sporadic spikes | Pre-allocate buffers in `init()` | | Blocking I/O | Consistent high latency | Move to `.async_io()` node | | Lock contention | Spikes correlated with other nodes | Use `try_lock()` or lock-free channels | | Large computation | Always near budget | Move to `.compute()` with a longer budget | ```rust // Bad: allocating every tick fn tick(&mut self) { let data: Vec = self.sensor.read_all(); // allocates self.process(&data); } // Good: pre-allocate, reuse buffer fn init(&mut self) { self.buffer = vec![0.0; 128]; // allocate once } fn tick(&mut self) { self.sensor.read_into(&mut self.buffer); // reuse self.process(&self.buffer); } ``` ## Workflow 2: "My Node Panicked" A node panic is caught by the scheduler. The node is marked `Unhealthy` and `on_error()` is called. ### Step 1: Check on_error() Output Implement `on_error()` on your node to log the error: ```rust impl Node for MotorCtrl { fn on_error(&mut self, error: &str) { eprintln!("MotorCtrl error: {error}"); // Optionally: enter safe state, publish error topic } } ``` ### Step 2: Get a Full Backtrace ```bash RUST_BACKTRACE=1 ./target/release/my_robot ``` ### Step 3: Reproduce with Deterministic Mode Use deterministic mode and `tick_once()` to replay the exact scenario: ```rust let mut scheduler = Scheduler::new() .deterministic(true) .tick_rate(100_u64.hz()); scheduler.add(MotorCtrl::new()).build()?; // Step through ticks one at a time for i in 0..1000 { println!("tick {i}"); scheduler.tick_once(); // panics are reproducible } ``` Use `tick(&["motor_ctrl"])` to isolate a single node. ### Step 4: Fix the Panic Fix the bug directly if it is in your code. For panics in third-party code, wrap with `catch_unwind`: ```rust fn tick(&mut self) { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { self.flaky_library.update(); })); if let Err(e) = result { eprintln!("Library panicked: {e:?}"); } } ``` ## Workflow 3: "My System Is Slow" The system runs but cannot keep up with its tick rate. ### Step 1: Find the Slowest Node ```rust let report = scheduler.profile(1000)?; println!("{report}"); // Nodes are listed with median and p99 timing // The node with the highest p99 is your bottleneck for node in &report.nodes { println!("{}: median={:?} p99={:?}", node.name, node.median, node.p99); } ``` Sort by `p99` to find the node causing the most delay. ### Step 2: Check Execution Classes A common mistake is running heavy work as `BestEffort` (the default), which blocks the main thread: ```rust // Bad: heavy computation blocks the main loop scheduler.add(PathPlanner::new()).build()?; // Good: run on a compute thread pool scheduler.add(PathPlanner::new()) .compute() .build()?; // Good: run blocking I/O on an async thread scheduler.add(CameraDriver::new()) .async_io() .build()?; ``` ### Step 3: Check CPU and Profile ```bash # Per-core CPU usage — if one core is 100% while others idle, use .compute() mpstat -P ALL 1 5 # Profile with perf to find hot functions perf record -g ./target/release/my_robot && perf report ``` | Symptom | Likely Cause | Fix | |---------|-------------|-----| | One core at 100% | Work not distributed | Use `.compute()`, `.cores(&[...])` | | Periodic spikes ~1s | Allocator pressure | Use `jemalloc`, pre-allocate | | Latency grows over time | Memory leak | Monitor RSS, fix leaking buffers | | Random multi-ms stalls | Page faults | `.require_rt()` calls `mlockall` | ## See Also - [Logging](/development/logging) - Structured logging with hlog! - [BlackBox Flight Recorder](/advanced/blackbox) - Post-mortem tick analysis - [Monitor](/development/monitor) - Real-time visual debugging - [Troubleshooting](/troubleshooting) - Common issues and solutions --- ## Error Handling Path: /development/error-handling Description: Unified error types, result handling, and best practices for HORUS applications # Error Handling HORUS provides a unified error handling system built on Rust's `Result` type, with rich error contexts and helpful diagnostics. ## Quick Start ```rust use horus::prelude::*; fn my_function() -> Result<()> { // Your code here Ok(()) } ``` The prelude exports these error types: - `Error` - The main error enum (short alias for `HorusError`) - `Result` - Alias for `std::result::Result` (short alias for `HorusResult`) The long names `HorusError` and `HorusResult` still work for backward compatibility, but new code should prefer the short aliases. ## Core Error Types ### Error The main error type for all HORUS operations (also available as `HorusError` for backward compatibility): ### Error Variants Each variant wraps a structured **sub-error enum** with specific fields for pattern matching: | Variant | Sub-error type | Domain | |---------|---------------|--------| | `Io(std::io::Error)` | — | File system and I/O errors | | `Config(ConfigError)` | `ConfigError` | Configuration parsing/validation | | `Communication(CommunicationError)` | `CommunicationError` | IPC, topics, network | | `Node(NodeError)` | `NodeError` | Node lifecycle (init, tick, shutdown) | | `Memory(MemoryError)` | `MemoryError` | SHM, mmap, tensor pools | | `Serialization(SerializationError)` | `SerializationError` | JSON, YAML, TOML, binary | | `NotFound(NotFoundError)` | `NotFoundError` | Missing frames, topics, nodes | | `Resource(ResourceError)` | `ResourceError` | Already exists, permission denied, unsupported | | `InvalidInput(ValidationError)` | `ValidationError` | Out-of-range, invalid format, constraints | | `Parse(ParseError)` | `ParseError` | Integer, float, boolean parsing | | `InvalidDescriptor(String)` | — | Cross-process tensor descriptor validation | | `Transform(TransformError)` | `TransformError` | Extrapolation, stale data | | `Timeout(TimeoutError)` | `TimeoutError` | Operation exceeded time limit | | `Internal { message, file, line }` | — | Internal errors with source location | | `Contextual { message, source }` | — | Error with preserved source chain | ## Creating Errors ### Using Constructors `Error` provides convenience constructors for the most common variants: ```rust use horus::prelude::*; // Configuration error let err = Error::config("Invalid frequency: must be positive"); // Node error with context (takes node name + message) let err = Error::node("MotorController", "Failed to initialize PWM"); // Network fault (communication sub-type) let err = Error::network_fault("192.168.1.100", "Connection refused"); // Internal error (prefer horus_internal! macro for file/line capture) let err = horus_internal!("Unexpected state reached"); ``` ### Using Variants Directly For errors without convenience constructors, construct the sub-error directly: ```rust use horus::prelude::*; use horus::error::{ResourceError, NotFoundError, CommunicationError}; let err = Error::Resource(ResourceError::PermissionDenied { resource: "/dev/ttyUSB0".into(), required_permission: "read/write".into(), }); let err = Error::Resource(ResourceError::AlreadyExists { resource_type: "session".into(), name: "main".into(), }); let err = Error::NotFound(NotFoundError::Topic { name: "cmd_vel".into(), }); ``` ### Internal Errors with Source Location Use the `horus_internal!()` macro to create internal errors that automatically capture file and line number: ```rust use horus::prelude::*; // Captures file/line automatically return Err(horus_internal!("Unexpected state: {:?}", state)); // Produces: Internal { message: "Unexpected state: ...", file: "src/foo.rs", line: 42 } ``` ### Contextual Errors with Source Chain Use `Error::Contextual` to wrap errors with additional context while preserving the original error chain: ```rust use horus::prelude::*; let config = load_file("robot.yaml") .map_err(|e| Error::Contextual { message: "Failed to load robot configuration".to_string(), source: Box::new(e), })?; // Produces: "Failed to load robot configuration\n Caused by: " ``` ## Error Context The `HorusContext` trait lets you wrap errors with descriptive context: ```rust use horus::prelude::*; fn load_config(path: &str) -> Result { let data = std::fs::read_to_string(path) .horus_context(format!("Failed to read config from {}", path))?; let config: Config = toml::from_str(&data) .horus_context("Invalid TOML in config file")?; Ok(config) } ``` | Method | Description | |--------|-------------| | `.horus_context(msg)` | Wrap error with a static context message | | `.horus_context_with(\|\| format!(...))` | Wrap with a lazily-evaluated message (avoids allocation on success) | Works on any `Result` where `E: std::error::Error`. ## Error Propagation ### Using the `?` Operator ```rust use horus::prelude::*; fn load_robot_config(path: &str) -> Result { // File I/O errors automatically convert to Error::Io let content = std::fs::read_to_string(path)?; // JSON errors automatically convert to Error::Serialization let config: Config = serde_json::from_str(&content)?; Ok(config) } ``` ### Automatic Conversions `Error` implements `From` for many common error types: | Source Type | Target Variant | |-------------|----------------| | `std::io::Error` | `Error::Io` | | `serde_json::Error` | `Error::Serialization` | | `serde_yaml::Error` | `Error::Serialization` | | `toml::de::Error` | `Error::Config` | | `toml::ser::Error` | `Error::Serialization` | | `std::num::ParseIntError` | `Error::Parse` | | `std::num::ParseFloatError` | `Error::Parse` | | `std::str::ParseBoolError` | `Error::Parse` | | `uuid::Error` | `Error::Internal` | | `std::sync::PoisonError` | `Error::Internal` | | `Box` | `Error::Internal` | | `Box` | `Error::Contextual` | | `anyhow::Error` | `Error::Internal` | ## Error Checking ### Pattern Matching ```rust use horus::prelude::*; use horus::error::{NotFoundError, NodeError, ResourceError}; match result { Ok(value) => process(value), Err(Error::NotFound(NotFoundError::Topic { name })) => { eprintln!("Topic not found: {}", name); } Err(Error::Node(node_err)) => { eprintln!("Node error: {}", node_err); } Err(Error::Resource(ResourceError::PermissionDenied { resource, .. })) => { eprintln!("Permission denied: {}", resource); } Err(Error::Internal { message, file, line }) => { eprintln!("Internal error at {}:{}: {}", file, line, message); } Err(e) => { eprintln!("Unexpected error: {}", e); if let Some(hint) = e.help() { eprintln!(" hint: {}", hint); } } } ``` ## Best Practices ### 1. Use Specific Error Types ```rust // Good: Specific error with context return Err(Error::node("IMU", "I2C read failed on register 0x3B")); // Avoid: Internal without context return Err(horus_internal!("something went wrong")); ``` ### 2. Add Context When Propagating ```rust fn initialize_sensor() -> Result<()> { open_i2c_bus().map_err(|e| { Error::node("IMU", format!("Failed to open I2C: {}", e)) })?; Ok(()) } ``` ### 3. Handle Expected Errors Gracefully ```rust fn get_config() -> Result { match load_config_file("config.yaml") { Ok(config) => Ok(config), Err(Error::NotFound(_)) => { // Expected: use defaults Ok(Config::default()) } Err(e) => Err(e), // Propagate unexpected errors } } ``` ### 4. Log Errors Before Propagating ```rust use horus::prelude::*; fn critical_operation() -> Result<()> { match do_something_important() { Ok(result) => Ok(result), Err(e) => { hlog!(error, "Critical operation failed: {}", e); Err(e) } } } ``` ## Node Error Handling ### In Tick Methods ```rust impl Node for MyNode { fn tick(&mut self) { // Handle errors in tick - don't propagate if let Err(e) = self.process_data() { hlog!(error, "Processing failed: {}", e); // Optionally publish status self.publish_error_status(e); } } } ``` ### Initialization Errors ```rust impl MyNode { pub fn new(config: Config) -> Result { let driver = config.driver.connect().map_err(|e| { Error::node("MyNode", format!("Driver init failed: {}", e)) })?; Ok(Self { driver }) } } ``` ### Graceful Degradation ```rust fn read_sensor(&mut self) -> Option { match self.backend.read() { Ok(data) => Some(data), Err(e) => { self.error_count += 1; if self.error_count > 10 { hlog!(error, "Sensor failing repeatedly: {}", e); } None // Return None instead of crashing } } } ``` ## Testing Error Handling ```rust #[cfg(test)] mod tests { use super::*; use horus::prelude::*; #[test] fn test_returns_not_found_for_missing_file() { let result = load_config("nonexistent.yaml"); assert!(matches!(result, Err(Error::NotFound(_)))); } #[test] fn test_returns_config_error_for_invalid_yaml() { let result = parse_config("invalid: [yaml"); assert!(matches!(result, Err(Error::Config(_)))); } #[test] fn test_error_context() { let err = Error::node("TestNode", "test message"); let display = format!("{}", err); assert!(display.contains("TestNode")); assert!(display.contains("test message")); } } ``` ## Integration with anyhow For applications that prefer `anyhow`: ```rust use anyhow::{Context, Result as AnyhowResult}; use horus::prelude::*; fn load_robot() -> AnyhowResult { let config = load_config("robot.yaml") .context("Failed to load robot configuration")?; let robot = Robot::from_config(config) .context("Failed to create robot from config")?; Ok(robot) } // Convert back to horus::Result if needed fn horus_function() -> Result { load_robot().map_err(|e| Error::from(e)) } ``` ## HorusError Variants Reference The `HorusError` enum (aliased as `Error`) is `#[non_exhaustive]` and wraps structured sub-error types: | Variant | Wraps | Example sub-variants | |---------|-------|----------------------| | `Io(std::io::Error)` | std I/O error | — | | `Config(ConfigError)` | Config parsing/validation | `MissingField`, `ParseFailed`, `Other` | | `Communication(CommunicationError)` | IPC, topics, network | `TopicFull`, `TopicNotFound`, `NetworkFault` | | `Node(NodeError)` | Node lifecycle | `InitPanic`, `InitFailed`, `TickFailed`, `Other { node, message }` | | `Memory(MemoryError)` | SHM, tensor pools | `PoolExhausted`, `ShmCreateFailed`, `MmapFailed`, `AllocationFailed` | | `Serialization(SerializationError)` | Serde errors | `Json`, `Yaml`, `Toml`, `Binary` | | `NotFound(NotFoundError)` | Missing resources | `Frame`, `Topic`, `Node`, `Service`, `Parameter` | | `Resource(ResourceError)` | Resource lifecycle | `AlreadyExists`, `PermissionDenied`, `Unsupported` | | `InvalidInput(ValidationError)` | Input validation | `OutOfRange`, `InvalidFormat`, `InvalidEnum`, `MissingRequired` | | `Parse(ParseError)` | Parsing failures | `Int`, `Float`, `Bool`, `Custom` | | `InvalidDescriptor(String)` | Tensor descriptor | — | | `Transform(TransformError)` | TF errors | `Extrapolation`, `StaleData` | | `Timeout(TimeoutError)` | Timeouts | — | | `Internal { message, file, line }` | Debug errors | — | | `Contextual { message, source }` | Error chains | — | ### Sub-Error Variant Details #### ConfigError | Variant | Fields | Severity | |---------|--------|----------| | `ParseFailed` | `format: &'static str`, `reason: String` | Permanent | | `MissingField` | `field: String`, `context: Option` | Permanent | | `ValidationFailed` | `field: String`, `expected: String`, `actual: String` | Permanent | | `InvalidValue` | `key: String`, `reason: String` | Permanent | | `Other(String)` | error message | Permanent | #### CommunicationError | Variant | Fields | Severity | |---------|--------|----------| | `TopicFull` | `topic: String` | Transient | | `TopicNotFound` | `topic: String` | Permanent | | `TopicCreationFailed` | `topic: String`, `reason: String` | Permanent | | `NetworkFault` | `peer: String`, `reason: String` | Transient | | `SerializationFailed` | `reason: String` | Permanent | | `ActionFailed` | `reason: String` | Permanent | #### NodeError | Variant | Fields | Severity | |---------|--------|----------| | `InitPanic` | `node: String` | Fatal | | `ReInitPanic` | `node: String` | Fatal | | `ShutdownPanic` | `node: String` | Permanent | | `InitFailed` | `node: String`, `reason: String` | Permanent | | `TickFailed` | `node: String`, `reason: String` | Permanent | | `Other` | `node: String`, `message: String` | Permanent | #### MemoryError | Variant | Fields | Severity | |---------|--------|----------| | `PoolExhausted` | `reason: String` | Transient | | `AllocationFailed` | `reason: String` | Permanent | | `ShmCreateFailed` | `path: String`, `reason: String` | Permanent | | `MmapFailed` | `reason: String` | Permanent | | `DLPackImportFailed` | `reason: String` | Permanent | | `OffsetOverflow` | (no fields) | Permanent | #### SerializationError | Variant | Fields | Severity | |---------|--------|----------| | `Json` | `source: serde_json::Error` | Permanent | | `Yaml` | `source: serde_yaml::Error` | Permanent | | `Toml` | `source: toml::ser::Error` | Permanent | | `Other` | `format: String`, `reason: String` | Permanent | #### NotFoundError | Variant | Fields | Severity | |---------|--------|----------| | `Frame` | `name: String` | Permanent | | `ParentFrame` | `name: String` | Permanent | | `Topic` | `name: String` | Permanent | | `Node` | `name: String` | Permanent | | `Service` | `name: String` | Permanent | | `Action` | `name: String` | Permanent | | `Parameter` | `name: String` | Permanent | | `Other` | `kind: String`, `name: String` | Permanent | #### ResourceError | Variant | Fields | Severity | |---------|--------|----------| | `AlreadyExists` | `resource_type: String`, `name: String` | Permanent | | `PermissionDenied` | `resource: String`, `required_permission: String` | Permanent | | `Unsupported` | `feature: String`, `reason: String` | Permanent | #### ValidationError | Variant | Fields | Severity | |---------|--------|----------| | `OutOfRange` | `field: String`, `min: String`, `max: String`, `actual: String` | Permanent | | `InvalidFormat` | `field: String`, `expected_format: String`, `actual: String` | Permanent | | `InvalidEnum` | `field: String`, `valid_options: String`, `actual: String` | Permanent | | `MissingRequired` | `field: String` | Permanent | | `ConstraintViolation` | `field: String`, `constraint: String` | Permanent | | `InvalidValue` | `field: String`, `value: String`, `reason: String` | Permanent | | `Conflict` | `field_a: String`, `field_b: String`, `reason: String` | Permanent | | `Other(String)` | error message | Permanent | #### ParseError | Variant | Fields | Severity | |---------|--------|----------| | `Int` | `input: String`, `source: ParseIntError` | Permanent | | `Float` | `input: String`, `source: ParseFloatError` | Permanent | | `Bool` | `input: String`, `source: ParseBoolError` | Permanent | | `Custom` | `type_name: String`, `input: String`, `reason: String` | Permanent | #### TransformError | Variant | Fields | Severity | |---------|--------|----------| | `Extrapolation` | `frame: String`, `requested_ns: u64`, `oldest_ns: u64`, `newest_ns: u64` | Permanent | | `Stale` | `frame: String`, `age: Duration`, `threshold: Duration` | Transient | #### TimeoutError (struct) | Field | Type | |-------|------| | `resource` | `String` | | `elapsed` | `Duration` | | `deadline` | `Option` | Severity: Transient All sub-error enums are `#[non_exhaustive]` — new variants may be added in future releases. ### Constructing Errors ```rust // Named constructors (3 available) Error::config("Invalid YAML syntax"); Error::node("SensorNode", "Sensor not responding"); Error::network_fault("192.168.1.100", "Connection refused"); // Internal errors (captures file and line automatically) horus_internal!("Unexpected state: {:?}", state); // Contextual errors (wrapping another error) Error::Contextual { message: "Failed to initialize sensor".into(), source: Box::new(io_error), }; ``` ### Type Aliases ```rust pub type HorusResult = Result; pub type Result = HorusResult; // Convenience alias pub type Error = HorusError; // Short name ``` ## Rate and Stopwatch Utilities ### Rate Drift-compensated rate limiter for controlling loop frequency: ```rust use horus::prelude::*; let mut rate = Rate::new(100.0); // Target 100 Hz loop { do_work(); rate.sleep(); // Sleeps for the remainder of the 10ms period } // Check actual performance println!("Actual: {:.1} Hz", rate.actual_hz()); println!("Late: {}", rate.is_late()); ``` | Method | Description | |--------|-------------| | `Rate::new(hz)` | Create targeting `hz` frequency | | `rate.sleep()` | Sleep for remainder of current period (drift-compensated) | | `rate.actual_hz()` | Exponentially smoothed actual frequency | | `rate.target_hz()` | Configured target frequency | | `rate.period()` | Target period as `Duration` | | `rate.reset()` | Reset cycle start (after long pauses) | | `rate.is_late()` | Whether current cycle exceeded target | ### Stopwatch Simple elapsed time tracker: ```rust use horus::prelude::*; let mut sw = Stopwatch::start(); expensive_operation(); println!("Took {:.2} ms", sw.elapsed_ms()); // Lap: return elapsed and reset let lap_time = sw.lap(); ``` | Method | Description | |--------|-------------| | `Stopwatch::start()` | Create and start immediately | | `sw.elapsed()` | Elapsed time as `Duration` | | `sw.elapsed_us()` | Elapsed microseconds (`u64`) | | `sw.elapsed_ms()` | Elapsed milliseconds (`f64`) | | `sw.reset()` | Reset to zero | | `sw.lap()` | Return elapsed and reset | ## Retry Configuration ### RetryConfig Configuration for automatic retry of transient errors with exponential backoff: ```rust use horus::prelude::*; // Default: 3 retries, 10ms initial backoff, 2x multiplier, 1s cap let config = RetryConfig::default(); // Custom let config = RetryConfig::new(5, 20_u64.ms()) .with_max_backoff(500_u64.ms()) .with_multiplier(1.5); ``` | Method | Returns | Description | |--------|---------|-------------| | `RetryConfig::new(max_retries, initial_backoff)` | `Self` | Create with 2x multiplier and 1s cap | | `.with_max_backoff(duration)` | `Self` | Set maximum backoff duration | | `.with_multiplier(f64)` | `Self` | Set backoff multiplier (must be positive and finite) | | `max_retries()` | `u32` | Maximum retry attempts | | `initial_backoff()` | `Duration` | Initial backoff before first retry | | `max_backoff()` | `Duration` | Maximum backoff cap | | `backoff_multiplier()` | `f64` | Multiplier applied after each retry | Default values: 3 retries, 10ms initial backoff, 2x multiplier, 1s max backoff. ### retry_transient() Generic retry function that only retries transient errors: ```rust use horus::prelude::*; let config = RetryConfig::new(3, 10_u64.ms()); let result = retry_transient(&config, || { some_operation_that_may_fail() })?; ``` **Signature:** ```rust pub fn retry_transient(config: &RetryConfig, f: F) -> HorusResult where F: FnMut() -> HorusResult, ``` **Behavior:** - Calls `f()` up to `max_retries + 1` times (initial attempt + retries) - Only `Severity::Transient` errors trigger retry (with exponential backoff) - `Severity::Permanent` and `Severity::Fatal` errors propagate immediately ### Error Severity Each error variant has an associated severity that determines retry behavior: | Severity | Retry? | Examples | |----------|--------|----------| | `Transient` | Yes | `TopicFull`, `NetworkFault`, `PoolExhausted`, `Timeout`, `Stale` | | `Permanent` | No | `TopicNotFound`, `MissingField`, `PermissionDenied`, `InitFailed` | | `Fatal` | No | `Internal`, `Io` | `retry_transient` and `ServiceClient::call_resilient` both use this severity classification. --- ## Quick Recipes ### Recipe: Hardware Init with Retry and Fallback ```rust fn init(&mut self) -> Result<()> { let config = RetryConfig::new(3, 100_u64.ms()); match retry_transient(&config, || open_hardware("/dev/ttyUSB0")) { Ok(hw) => { self.hardware = Some(hw); hlog!(info, "Hardware connected"); } Err(e) => { hlog!(warn, "Hardware unavailable, using simulation: {}", e); self.hardware = None; // Fallback to sim mode } } Ok(()) } ``` ### Recipe: Pattern-Match Specific Errors ```rust match Topic::::new("imu") { Ok(topic) => { /* use topic */ } Err(Error::Communication(CommunicationError::TopicCreationFailed { topic, reason })) => { hlog!(error, "Cannot create topic '{}': {} — check SHM permissions", topic, reason); } Err(e) => { hlog!(error, "Unexpected error: {}", e); } } ``` ### Recipe: Conditional Stop on Repeated Failures ```rust fn tick(&mut self) { match self.sensor.read() { Ok(data) => { self.consecutive_failures = 0; self.process(data); } Err(e) => { self.consecutive_failures += 1; hlog!(warn, "Sensor read failed ({}/5): {}", self.consecutive_failures, e); if self.consecutive_failures >= 5 { hlog!(error, "Too many failures, entering safe state"); self.enter_safe_state(); } } } } ``` --- ## See Also - [Error Types Reference](/rust/api/error-types) - Complete error variant listing - [Core Concepts](/concepts/core-concepts-nodes) - Understanding HORUS nodes - [API Reference](/rust/api) - Core API including Error types - [Services API](/rust/api/services) - `call_resilient` uses RetryConfig - [Troubleshooting](/troubleshooting) - Common issues and solutions ======================================== # SECTION: Advanced Topics ======================================== --- ## BlackBox Flight Recorder Path: /advanced/blackbox Description: Event recording and post-mortem debugging for HORUS applications # BlackBox Flight Recorder ## Why You Need This Your robot runs overnight in a warehouse. At 3:17 AM, it stops moving. By morning, the logs have rotated, the process restarted, and nobody knows what happened. The **BlackBox** solves this. Like an aircraft's flight data recorder, it continuously records the last N events in a fixed-size ring buffer. When something goes wrong, the BlackBox contains the exact sequence of events leading up to the failure — deadline misses, node panics, budget violations, emergency stops — all timestamped and structured. **BlackBox vs Logging:** Logs are text, grow forever, and require parsing. The BlackBox is structured events, fixed-size (never fills your disk), and queryable by type (show only anomalies). **BlackBox vs Record/Replay:** Record/Replay captures full node state (inputs/outputs) for deterministic replay — great for debugging but storage-heavy. The BlackBox captures lightweight events (what happened, not the full data) — always-on, zero overhead, crash-safe. ## When to Use | Situation | Use BlackBox? | |-----------|--------------| | Robot runs unattended (production, field tests) | **Yes** — you need crash forensics | | Safety-critical system (motors, arms, drones) | **Yes** — every deadline miss is recorded | | Development with debugger attached | Optional — you can inspect directly | | Short test runs (< 5 minutes) | Optional — logs are usually sufficient | | Overnight regression testing | **Yes** — find intermittent failures | ## How It Works The BlackBox is a **circular buffer** — it keeps the last N events and discards the oldest when full. This means: - **Fixed memory** — never grows beyond the configured size - **Always-on** — no performance impact (events are tiny structs) - **Crash-safe** — data persists even if the process is killed - **No manual instrumentation** — the Scheduler records events automatically ## Enabling BlackBox Use the `.blackbox(size_mb)` builder method to enable the BlackBox: ```rust use horus::prelude::*; // 16MB black box for general production let mut scheduler = Scheduler::new() .blackbox(16); // 1GB black box for safety-critical systems with watchdog let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(1024); // 100MB black box for hard real-time systems let mut scheduler = Scheduler::new() .blackbox(100); ``` ## What Gets Recorded The BlackBox automatically captures events during scheduler execution: | Event | Description | |-------|-------------| | Scheduler start/stop | When the scheduler begins and ends | | Node execution | Each node tick with duration and success/failure | | Node errors | Failed node executions | | Deadline misses | Nodes that missed their timing deadline | | Budget violations | Nodes that exceeded their execution time budget | | Failure policy events | Failure policy state transitions | | Emergency stops | Safety system activations | | Custom events | User-defined markers | ## Post-Mortem Debugging After a failure, the BlackBox contains the sequence of events leading up to it. Use the Scheduler's blackbox access to inspect: ```rust use horus::prelude::*; let mut scheduler = Scheduler::new() .blackbox(16); // ... application runs ... // After a failure, inspect the blackbox if let Some(bb) = scheduler.get_blackbox() { let bb = bb.lock().unwrap(); // Get all anomalies (errors, deadline misses, e-stops) let anomalies = bb.anomalies(); println!("=== ANOMALIES ({}) ===", anomalies.len()); for record in &anomalies { println!("[tick {}] {:?}", record.tick, record.event); } // Get all events (full history) let all_events = bb.events(); println!("\n=== LAST 20 EVENTS ==="); for record in all_events.iter().rev().take(20) { println!("[tick {}] {:?}", record.tick, record.event); } } ``` ## Circular Buffer Behavior The BlackBox uses a fixed-size circular buffer. When full, the oldest events are discarded: ``` Buffer capacity: 50,000 records (10MB) Event 1 → [1, _, _, _, _] New events fill the buffer Event 2 → [1, 2, _, _, _] ... Event N → [1, 2, ..., N-1, N] Buffer full Event N+1 → [2, 3, ..., N, N+1] Oldest dropped ``` This ensures bounded memory usage while keeping the most recent events for debugging. ## Recommended Buffer Sizes | Use Case | Configuration | Buffer Size | |----------|---------------|-------------| | Development | `.blackbox(16)` | 16 MB | | Long-running production | `.blackbox(100)` | 100 MB | | Safety-critical | `.blackbox(1024)` | 1 GB | ## CLI Usage Inspect the BlackBox from the command line: ```bash # View all events horus blackbox # View anomalies only (errors, deadline misses, e-stops) horus blackbox --anomalies # Follow in real-time (like tail -f) horus blackbox --follow # Filter by node horus blackbox --node motor_ctrl # Filter by event type horus blackbox --event DeadlineMiss # JSON output for scripts/dashboards horus blackbox --json ``` ## Debugging Walkthrough: "My Robot Crashed Overnight" **Scenario:** Your mobile robot stopped moving during an overnight warehouse test. The process restarted but the original crash data is gone. **Step 1: Check the BlackBox** ```bash horus blackbox --anomalies ``` **Step 2: Read the timeline** ``` [03:17:01.001] SchedulerStart { nodes: 4, rate: 500Hz } [03:17:01.500] NodeTick { name: "planner", duration_us: 2100, success: true } [03:17:01.502] DeadlineMiss { name: "collision_checker", deadline_us: 1900, actual_us: 4200 } [03:17:01.503] DeadlineMiss { name: "collision_checker", deadline_us: 1900, actual_us: 5100 } [03:17:01.504] NodeError { name: "arm_controller", error: "joint limit exceeded" } [03:17:01.504] EmergencyStop { reason: "deadline miss threshold exceeded" } ``` **Step 3: Diagnose** The collision checker started missing its 1.9ms deadline (taking 4-5ms instead). During that time, the planner sent a trajectory that would have been rejected — but the check arrived too late. The arm exceeded its joint limits. **Step 4: Fix** - Tighten the collision checker's budget: `.budget(1500_u64.us())` - Or add a safety interlock: hold trajectory execution until collision check completes - Or move collision checking to the same RT thread as the arm controller ## BlackBox vs Other Debugging Tools | Tool | What it captures | Storage | When to use | |------|-----------------|---------|-------------| | **BlackBox** | Scheduler events (lightweight) | Fixed ring buffer (16-1024 MB) | Always-on crash forensics | | **Record/Replay** | Full node state (inputs/outputs) | Grows with time | Reproduce specific bugs | | **horus log** | Text log messages | Grows with time | Verbose debugging | | **horus monitor** | Live system state | None (real-time only) | Active debugging | ## See Also - [Safety Monitor](/advanced/safety-monitor) — Real-time safety monitoring - [Fault Tolerance](/advanced/circuit-breaker) — Failure policies and recovery - [Record & Replay](/advanced/record-replay) — Full recording and playback - [Debugging Workflows](/development/debugging) — Step-by-step debugging guides --- ## Record & Replay Path: /advanced/record-replay Description: Recording and tick-perfect replay for debugging, testing, and analysis # Record & Replay HORUS provides a record/replay system for capturing node execution and replaying it with tick-perfect determinism. This enables debugging workflows including time travel, mixed replay, and comparing behavior between runs. ## Overview The record/replay system supports: - **Full recording**: Capture entire system execution - **Tick-perfect replay**: Reproduce exact behavior deterministically - **Time travel**: Jump to any recorded tick - **Mixed replay**: Combine recorded nodes with live execution - **Playback control**: Speed adjustment, tick ranges ## Enabling Recording ### Via Builder API Enable recording through builder methods: ```rust use horus::prelude::*; // Enable recording via builder API let mut scheduler = Scheduler::new() .with_recording(); ``` ### Via CLI ```bash # Record during a run horus run --record my_session my_project ``` When recording is enabled, the scheduler automatically captures each node's inputs, outputs, and timing. ## Replaying Recordings ### Full Replay Replay an entire recorded session: ```rust,ignore use horus::prelude::*; use std::path::PathBuf; // Load and replay an entire scheduler recording let mut scheduler = Scheduler::replay_from( PathBuf::from("~/.horus/recordings/crash/scheduler@abc123.horus") )?; scheduler.run()?; ``` ### Time Travel Jump to specific tick ranges during replay: ```rust,ignore // Start at a specific tick let mut scheduler = Scheduler::replay_from(path)? .start_at_tick(1500); // Stop at a specific tick let mut scheduler = Scheduler::replay_from(path)? .stop_at_tick(2000); // Adjust playback speed (0.01 to 100.0) let mut scheduler = Scheduler::replay_from(path)? .with_replay_speed(0.5); // Half speed ``` ### Mixed Replay Combine recorded nodes with live execution for what-if testing: ```rust,ignore use horus::prelude::*; let mut scheduler = Scheduler::new(); // Add replay nodes from recordings scheduler.add_replay( PathBuf::from("recordings/Lidar@001.horus"), 0, // priority )?; // Add live nodes alongside scheduler.add(live_controller).order(1).build()?; scheduler.run()?; ``` ### Output Overrides Override specific outputs during replay: ```rust,ignore let mut scheduler = Scheduler::replay_from(path)? .with_override("sensor_node", "temperature", 25.0f32.to_le_bytes().to_vec()); ``` ## CLI Commands Record and replay from the command line: ```bash # Start recording during a run horus run --record my_session my_project # List recording sessions horus record list horus record list --long # Show file sizes and tick counts # Show details of a session horus record info my_session # Replay a recording horus record replay my_session horus record replay my_session --start-tick 1000 --stop-tick 2000 horus record replay my_session --speed 0.5 # Compare two recording sessions horus record diff session1 session2 horus record diff session1 session2 --limit 50 # Export to JSON or CSV horus record export my_session --output data.json --format json horus record export my_session --output data.csv --format csv # Inject recorded nodes into a new run horus record inject my_session --nodes camera_node,lidar_node horus record inject my_session --all --loop # Delete a recording session horus record delete my_session horus record delete my_session --force ``` ## Managing Recordings ```rust,ignore // List all recording sessions let sessions = Scheduler::list_recordings()?; // Delete a recording session Scheduler::delete_recording("old_session")?; ``` ## `replay_from` vs `add_replay` | Method | Use Case | Clock | |--------|----------|-------| | `Scheduler::replay_from(path)` | Full replay — all nodes from one recording | ReplayClock (recorded timestamps) | | `scheduler.add_replay(path, priority)` | Mixed — replay some nodes, run others live | ReplayClock for replay nodes | **When to use which:** - Use `replay_from()` for **post-mortem debugging** — replay an entire session exactly as recorded - Use `add_replay()` for **regression testing** — replay recorded sensor data while running a new version of your planner/controller live ```rust,ignore // Post-mortem: "what happened in production?" let mut scheduler = Scheduler::replay_from("crash_session.hbag")?; scheduler.run()?; // Regression test: "does the new planner work with the same sensor data?" let mut scheduler = Scheduler::new(); scheduler.add_replay("sensor_data.horus".into(), 0)?; // recorded LiDAR + IMU scheduler.add(NewPlannerV2::new()).order(1).build()?; // live planner under test scheduler.run()?; ``` ## See Also - [BlackBox Flight Recorder](/advanced/blackbox) - Flight recorder for post-mortem debugging - [Scheduler Configuration](/advanced/scheduler-configuration) - SchedulerConfig and node configuration - [Clock & Time API](/rust/api/clock-api) - ReplayClock and time backends --- ## Fault Tolerance Path: /advanced/circuit-breaker Description: Per-node failure policies for preventing cascading failures — Fatal, Restart, Skip, Ignore with real robotics examples # Fault Tolerance Every node in a robot can fail — a sensor disconnects, a planning algorithm panics, a network request times out. HORUS failure policies control what happens next: stop the robot, restart the node, skip it temporarily, or ignore the error entirely. ## The Problem Without failure policies, one crashing node kills the entire system: ``` Tick 100: sensor_driver panics (USB disconnected) Tick 100: scheduler stops Tick 100: motor_controller stops receiving commands Result: robot stops moving in the middle of a task ``` With failure policies, the system adapts: ``` Tick 100: sensor_driver panics (USB disconnected) Tick 100: FailurePolicy::Restart → re-init sensor_driver (10ms backoff) Tick 101: sensor_driver panics again → restart (20ms backoff) Tick 102: USB reconnects → sensor_driver.init() succeeds → normal operation Result: robot paused briefly, then resumed automatically ``` ## The Four Policies ### Fatal — Stop Everything ```rust scheduler.add(motor_controller) .order(0) .rate(1000_u64.hz()) .failure_policy(FailurePolicy::Fatal) .build()?; ``` First failure stops the scheduler immediately. Use for nodes where continued operation after failure is **unsafe**: - Motor controllers (stale commands = uncontrolled motion) - Safety monitors (can't monitor safety if the monitor is broken) - Emergency stop handlers **When it triggers**: `node.tick()` panics or returns an error. The scheduler calls `stop()` and shuts down all nodes cleanly. ### Restart — Re-Initialize with Backoff ```rust scheduler.add(lidar_driver) .order(1) .rate(100_u64.hz()) .failure_policy(FailurePolicy::restart(3, 50_u64.ms())) .build()?; ``` Re-initializes the node with exponential backoff. After `max_restarts` exhausted, escalates to fatal stop. ``` failure 1 → restart, wait 50ms failure 2 → restart, wait 100ms (2x backoff) failure 3 → restart, wait 200ms (2x backoff) failure 4 → max_restarts exceeded → fatal stop ``` After a successful tick, the backoff timer clears. Use for nodes that can recover from transient failures: - Sensor drivers (hardware reconnection) - Network clients (server temporarily unavailable) - Camera nodes (USB reset) ### Skip — Tolerate with Cooldown ```rust scheduler.add(telemetry_uploader) .order(200) .async_io() .failure_policy(FailurePolicy::skip(5, 1_u64.secs())) .build()?; ``` After `max_failures` consecutive failures, the node is **suppressed** for the cooldown period. After cooldown, the node is allowed again and the failure counter resets. ``` failure 1 → continue failure 2 → continue failure 3 → continue failure 4 → continue failure 5 → node suppressed for 1 second ... 1 second passes ... node allowed again, failure count = 0 ``` Use for nodes whose absence doesn't affect core robot operation: - Logging and telemetry upload - Diagnostics reporting - Cloud sync - Non-critical monitoring ### Ignore — Swallow Failures ```rust scheduler.add(stats_collector) .order(100) .failure_policy(FailurePolicy::Ignore) .build()?; ``` Failures are completely ignored. The node keeps ticking every cycle regardless of errors. Use only when partial results are acceptable: - Statistics collectors (missing one sample is fine) - Best-effort visualization - Debug output nodes ## Severity-Aware Handling HORUS errors carry severity levels that can **override** the configured policy: | Severity | Effect | |----------|--------| | **Fatal** (e.g., shared memory corruption) | Always stops the scheduler, even with `Ignore` policy | | **Transient** (e.g., topic full, network timeout) | De-escalates `Fatal` policy to `Restart` (transient errors are recoverable) | | **Permanent** (e.g., invalid configuration) | Follows the configured policy | This means a safety-critical node with `Fatal` policy won't kill the system on a transient network glitch — it'll restart instead. But a shared-memory corruption always stops, even on an `Ignore` node. ## Complete Robot Example ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(500_u64.hz()) .prefer_rt() .watchdog(500_u64.ms()); // CRITICAL: Motor controller — stop if it fails scheduler.add(MotorController::new()) .order(0) .rate(500_u64.hz()) .on_miss(Miss::SafeMode) .failure_policy(FailurePolicy::Fatal) .build()?; // RECOVERABLE: Lidar driver — restart on USB disconnect scheduler.add(LidarDriver::new()) .order(1) .rate(100_u64.hz()) .failure_policy(FailurePolicy::restart(5, 100_u64.ms())) .build()?; // RECOVERABLE: Camera — restart up to 3 times scheduler.add(CameraNode::new()) .order(2) .rate(30_u64.hz()) .failure_policy(FailurePolicy::restart(3, 200_u64.ms())) .build()?; // NON-CRITICAL: Path planner — skip if it fails repeatedly scheduler.add(PathPlanner::new()) .order(5) .compute() .failure_policy(FailurePolicy::skip(3, 2_u64.secs())) .build()?; // BEST-EFFORT: Telemetry — ignore failures scheduler.add(TelemetryUploader::new()) .order(200) .async_io() .rate(1_u64.hz()) .failure_policy(FailurePolicy::Ignore) .build()?; scheduler.run() } ``` ## Choosing the Right Policy | Node Type | Policy | Why | |-----------|--------|-----| | Motor control, safety | `Fatal` | Unsafe to continue without these | | Sensor drivers | `Restart(3-5, 50-200ms)` | Hardware reconnects are common | | Perception pipelines | `Restart(3, 100ms)` or `Skip(5, 2s)` | Can recover or degrade gracefully | | Logging, telemetry | `Skip(5, 1s)` or `Ignore` | Non-critical, absence is tolerable | | Debug/visualization | `Ignore` | Partial results are fine | ## Monitoring Failures Failure events are recorded in the [BlackBox](/advanced/blackbox) flight recorder: ```bash # View failure events from the blackbox horus blackbox show --filter errors # Monitor live horus log -f --level error ``` In code: ```rust if let Some(bb) = scheduler.get_blackbox() { for record in bb.lock().unwrap().anomalies() { println!("[tick {}] {:?}", record.tick, record.event); } } ``` ## See Also - [Safety Monitor](/advanced/safety-monitor) — Watchdog and graduated degradation - [BlackBox Recorder](/advanced/blackbox) — Crash forensics - [Scheduler Configuration](/advanced/scheduler-configuration) — Per-node builder API --- ## Deterministic Mode Path: /advanced/deterministic-mode Description: Run-to-run reproducible execution with SimClock, dependency ordering, and deterministic RNG for simulation, testing, and replay # Deterministic Mode HORUS provides run-to-run deterministic execution: the same binary on the same hardware produces bit-identical outputs across unlimited runs. This is the industry standard for robotics simulation (Gazebo, Drake, Isaac Sim). ## Enabling Deterministic Mode ```rust use horus::prelude::*; let mut scheduler = Scheduler::new() .deterministic(true) // SimClock + dependency ordering .tick_rate(100_u64.hz()); scheduler.add(Controller::new()) .order(0) .rate(100_u64.hz()) .build()?; // Each tick_once() produces identical results every run for _ in 0..1000 { scheduler.tick_once()?; } ``` ## What Changes in Deterministic Mode | Aspect | Normal Mode | Deterministic Mode | |--------|-------------|-------------------| | Clock | Wall clock (real time) | Virtual SimClock (fixed dt per tick) | | RNG | System entropy | Tick-seeded (reproducible) | | Execution order | Parallel by execution class | Dependency-ordered steps | | Independent nodes | Parallel | Still parallel | | Dependent nodes | Parallel (races possible) | Sequenced (producer before consumer) | | Execution classes | All active | All active | | Failure policies | Active | Active | | Watchdog | Active | Active | **What does NOT change**: execution classes (RT, Compute, AsyncIo, Event, BestEffort), failure policies, watchdog, budget/deadline monitoring. Deterministic mode does not degrade the scheduling system — it adds ordering guarantees. ## Framework Time API Use `horus::now()`, `horus::dt()`, and `horus::rng()` instead of `Instant::now()` and `rand::random()`. These are the standard framework API — same pattern as `hlog!()` for logging. ```rust use horus::prelude::*; struct Controller { position: f64, velocity: f64, } impl Node for Controller { fn tick(&mut self) { // horus::dt() returns fixed 1/rate in deterministic mode, // real elapsed in normal mode let dt = horus::dt(); self.position += self.velocity * dt.as_secs_f64(); // horus::rng() is tick-seeded in deterministic mode, // system entropy in normal mode let noise: f64 = horus::rng(|r| { use rand::Rng; r.gen_range(-0.01..0.01) }); self.velocity += noise; hlog!(debug, "pos={:.3} at t={:?}", self.position, horus::elapsed()); } } ``` See the [Time API reference](/rust/time-api) for the full API. ## Dependency Ordering The scheduler builds a dependency graph from nodes' `publishers()` and `subscribers()` metadata. Dependent nodes are sequenced (producer before consumer). Independent nodes run in parallel. ```rust struct SensorDriver { scan_topic: Topic, } impl Node for SensorDriver { fn name(&self) -> &str { "sensor" } fn publishers(&self) -> Vec { vec![TopicMetadata { topic_name: "scan".into(), type_name: "LaserScan".into(), }] } fn tick(&mut self) { self.scan_topic.send(self.read_hardware()); } } struct Controller { scan_topic: Topic, cmd_topic: Topic, } impl Node for Controller { fn name(&self) -> &str { "controller" } fn subscribers(&self) -> Vec { vec![TopicMetadata { topic_name: "scan".into(), type_name: "LaserScan".into(), }] } fn publishers(&self) -> Vec { vec![TopicMetadata { topic_name: "cmd".into(), type_name: "CmdVel".into(), }] } fn tick(&mut self) { if let Some(scan) = self.scan_topic.try_recv() { let cmd = self.compute_velocity(&scan); self.cmd_topic.send(cmd); } } } ``` The scheduler automatically ensures `sensor` ticks before `controller` because `controller` subscribes to a topic that `sensor` publishes. ### Fallback Without Metadata If nodes don't implement `publishers()` / `subscribers()`, the scheduler uses `.order()` values as a proxy: lower order runs first, same order = independent (parallel). ## Normal vs Deterministic: When to Use Which | Purpose | Mode | Why | |---------|------|-----| | Real robot deployment | Normal | Wall clock matches hardware reality | | Simulation (physics engine) | Deterministic | Virtual clock matches physics time | | Unit / integration tests | Deterministic | Reproducible, no flakes | | CI pipeline | Deterministic | Same result every run | | Record/replay debugging | Replay (`replay_from()`) | Recorded clock reproduces exact scenario | | Recording a session on real robot | Normal + `.with_recording()` | Wall clock for hardware, recording for later | **Deterministic mode uses virtual time** — it cannot drive real hardware. A motor controller receiving `horus::dt()` in deterministic mode gets a fixed value (e.g., exactly 1ms for 1kHz), regardless of how fast ticks actually execute. This is correct for simulation but wrong for real actuators. ## Record and Replay ```rust // Record a session let mut scheduler = Scheduler::new() .deterministic(true) .with_recording() .tick_rate(100_u64.hz()); scheduler.add(Sensor::new()).order(0).build()?; scheduler.add(Controller::new()).order(1).build()?; scheduler.run_for(10_u64.secs())?; // Replay — bit-identical output let mut replay = Scheduler::replay_from( "~/.horus/recordings/session_001/scheduler@abc123.horus".into() )?; replay.run()?; // Mixed replay — recorded sensors, new controller let mut replay = Scheduler::replay_from(path)?; replay.add(ControllerV2::new()).order(1).rate(100_u64.hz()).build()?; replay.run()?; ``` During replay, recorded topic data is injected into shared memory so live subscriber nodes see the replayed data. ## Determinism Guarantees **What HORUS guarantees**: same binary + same hardware produces bit-identical outputs, tick for tick, across unlimited runs. **What is NOT deterministic** (hardware/compiler, not HORUS): - **Cross-platform float**: IEEE 754 differs across CPUs (FMA, extended precision). Same binary + same hardware = deterministic. - **Direct `Instant::now()`**: Bypasses the framework clock. Use `horus::now()` instead. - **`HashMap` iteration**: Rust randomizes per process. Use `BTreeMap` in deterministic nodes. --- ## Network Backends Path: /advanced/network-backends Description: Communication backends in HORUS — automatic local IPC with planned network transport # Communication Backends HORUS automatically selects the optimal communication backend based on topology — no configuration needed. All current backends are **local** (same-machine), using shared memory or in-process channels. ## Automatic Backend Selection When you call `Topic::new("name")`, HORUS automatically detects the optimal backend based on the number of publishers, subscribers, and whether they're in the same process: ```rust,ignore use horus::prelude::*; // Just create a topic — backend is auto-selected let topic = Topic::::new("motors.cmd_vel")?; topic.send(CmdVel { linear: 1.0, angular: 0.0 }); ``` No configuration needed. The backend upgrades and downgrades dynamically as participants join and leave. ## Communication Paths HORUS selects the optimal path based on where your nodes are running and how many publishers/subscribers are involved: ### Same-Process Communication | Scenario | Latency | |----------|---------| | Same thread, 1 publisher → 1 subscriber | ~3ns | | 1 publisher → 1 subscriber | ~18ns | | 1 publisher → many subscribers | ~24ns | | Many publishers → 1 subscriber | ~26ns | | Many publishers → many subscribers | ~36ns | ### Cross-Process Communication (Shared Memory) | Scenario | Latency | |----------|---------| | 1 publisher → 1 subscriber (simple types) | ~50ns | | Many publishers → 1 subscriber | ~65ns | | 1 publisher → many subscribers | ~70ns | | 1 publisher → 1 subscriber | ~85ns | | Many publishers → many subscribers | ~167ns | The path is selected based on: - **Process locality**: Same thread → same process → cross-process - **Topology**: Number of publishers and subscribers - **Data type**: Simple fixed-size types get the fastest cross-process path ## Dynamic Migration HORUS dynamically migrates between backends as topology changes: ``` Single publisher + single subscriber (same process) → ~18ns Second subscriber joins (same process) → ~24ns Subscriber in different process joins → ~70ns All subscribers disconnect except one in-process → ~18ns ``` Migration is transparent — `send()` and `recv()` calls are unaffected. ## Performance Characteristics | Metric | In-Process | Shared Memory | |--------|-----------|---------------| | Latency | 3-36ns | 50-167ns | | Throughput | Millions msg/s | Millions msg/s | | Zero-copy | Yes | Yes | | Cross-machine | No | No | ## Planned: Network Transport (Zenoh) Future versions of HORUS will add Zenoh-based network transport for: - **Multi-robot communication** across machines - **Cloud connectivity** for telemetry and remote monitoring - **ROS2 interoperability** via Zenoh DDS bridge The planned architecture adds network backends alongside the existing local backends: | Transport | Latency | Use Case | |-----------|---------|----------| | In-process | 3-36ns | Same-process nodes | | Shared Memory | 50-167ns | Same-machine IPC | | Zenoh (planned) | ~100μs+ | Multi-robot, cloud, ROS2 | When network transport is implemented, `Topic::new()` will continue to auto-select the optimal backend. Network transport will only be used when topics are explicitly configured for remote communication. ## See Also - [Topic](/concepts/core-concepts-topic) - Shared memory architecture and Topic API - [API Reference](/rust/api) - Topic creation and usage --- ## Scheduler Configuration Path: /advanced/scheduler-configuration Description: Configuring the HORUS scheduler with the fluent node builder API, execution classes, and per-node settings # Scheduler Configuration HORUS uses a single scheduler entry point — `Scheduler::new()` — with composable builder methods (`.watchdog()`, `.blackbox()`, `.require_rt()`, `.max_deadline_misses()`) and a fluent node builder API that gives you full control over execution class, timing, ordering, and failure handling on a per-node basis. ## Creating a Scheduler Every scheduler starts with `Scheduler::new()`. From there you can optionally set global parameters with builder methods before adding nodes: ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(1000_u64.hz()); // Global tick rate (default: 100 Hz) // ... add nodes ... scheduler.run()?; Ok(()) } ``` ### Builder Methods | Method | Description | Default | |--------|-------------|---------| | `.tick_rate(freq)` | Global scheduler tick rate | `100 Hz` | | `.deterministic(bool)` | Deterministic mode — SimClock, dependency ordering, seeded RNG. See [Deterministic Mode](/advanced/deterministic-mode) | `false` | | `.watchdog(Duration)` | Frozen node detection — auto-creates safety monitor | disabled | | `.blackbox(size_mb)` | BlackBox flight recorder (n MB ring buffer) | disabled | | `.max_deadline_misses(n)` | Emergency stop after n deadline misses | `100` | | `.require_rt()` | Hard real-time — panics without RT capabilities | — | | `.prefer_rt()` | Request RT features (degrades gracefully) | — | | `.cores(&[usize])` | Pin scheduler threads to specific CPU cores | all cores | | `.verbose(bool)` | Enable/disable non-emergency logging | `true` | | `.with_recording()` | Enable record/replay | — | | `.telemetry(endpoint)` | Export telemetry to UDP/file endpoint | disabled | ## Adding Nodes Add nodes with `scheduler.add(n)`, then chain configuration calls, and finalize with `.build()?`: ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(1000_u64.hz()); // Real-time motor control — runs first every tick // rate() auto-derives budget (80%) and deadline (95%), auto-marks as RT scheduler.add(MotorController::new("arm")) .order(0) .rate(1000_u64.hz()) .on_miss(Miss::SafeMode) .build()?; // Sensor node — high priority, custom rate scheduler.add(LidarDriver::new("/dev/lidar0")) .order(10) .rate(500_u64.hz()) .build()?; // Compute-heavy planning — runs on a worker thread scheduler.add(PathPlanner::new()) .order(50) .compute() .build()?; // Event-driven node — wakes only when the topic has new data scheduler.add(CollisionChecker::new()) .on("lidar.points") .build()?; // Async I/O — network or disk, never blocks the real-time loop scheduler.add(TelemetryUploader::new()) .order(200) .async_io() .rate(10_u64.hz()) .build()?; scheduler.run()?; Ok(()) } ``` ## Execution Classes Every node belongs to exactly one execution class. Set it in the builder chain: | Method | Class | Description | |--------|-------|-------------| | `.compute()` | Compute | Offloaded to a worker thread pool. Use for planning, SLAM, or ML inference. | | `.on(topic)` | Event-Driven | Wakes only when the named topic receives new data. | | `.async_io()` | Async I/O | Runs on an async executor. Use for network, disk, or cloud calls. | If no execution class is specified, the node defaults to **BestEffort**. A node is automatically promoted to the **RT** class when you set `.rate(Frequency)` (which auto-derives budget at 80% and deadline at 95% of the period). ### When to Use Each Class - **RT (auto-detected)** — Motor controllers, safety monitors, sensor fusion, anything that must run every tick with bounded latency. Triggered by `.rate(Frequency)` on a BestEffort node. - **`.compute()`** — Path planning, point cloud processing, ML inference. These can take longer than a single tick without blocking RT nodes. - **`.on(topic)`** — Collision detection, event handlers, reactive behaviors. Only runs when there is new data, saving CPU when idle. - **`.async_io()`** — Telemetry upload, log shipping, cloud API calls. Never blocks any real-time or compute work. **What each class means for your robot:** - **RT** — Your motor controller sends PWM commands every millisecond. Missing one cycle causes the motor to overshoot. This node needs a dedicated RT thread. - **Compute** — Your SLAM algorithm takes 50ms to process a lidar scan. If it runs on the RT thread, the motor controller misses 50 deadlines. Compute nodes run on a separate thread pool. - **Event** — Your collision detector only needs to run when new lidar data arrives, not every cycle. Event nodes sleep until their topic gets a message. - **AsyncIo** — Your telemetry node uploads data to a cloud server. Network calls can take seconds. AsyncIo nodes run on a tokio thread pool so they never block anything. - **BestEffort** — Your debug logger. Runs on the main thread when there's time, no timing guarantees. ## Per-Node Configuration ### Ordering and Timing | Method | Description | |--------|-------------| | `.order(n)` | Execution priority within a tick (lower = runs first) | | `.rate(Frequency)` | Node-specific tick rate — auto-derives budget (80%) and deadline (95%), auto-marks as RT | | `.budget(Duration)` | Override auto-derived tick budget (max execution time) | | `.deadline(Duration)` | Override auto-derived absolute deadline | | `.on_miss(Miss)` | What to do on deadline miss (`Miss::Warn`, `Miss::Skip`, `Miss::SafeMode`, `Miss::Stop`) | ### RT Configuration | Method | Description | |--------|-------------| | `.priority(i32)` | OS thread priority (SCHED_FIFO 1-99) for this node's RT thread | | `.core(usize)` | Pin this node's RT thread to a specific CPU core | | `.watchdog(Duration)` | Per-node watchdog timeout (overrides scheduler global) | These are only meaningful for RT nodes (nodes with `.rate()`). They degrade gracefully when RT capabilities are unavailable. ```rust // Safety-critical node: highest priority, pinned to core 2, tight watchdog scheduler.add(EmergencyStop::new()) .order(0) .rate(1000_u64.hz()) .priority(99) .core(2) .watchdog(2_u64.ms()) .on_miss(Miss::Stop) .build()?; // Logger: long watchdog, async I/O scheduler.add(Logger::new()) .order(200) .async_io() .watchdog(5_u64.secs()) .build()?; ``` ### Failure Policy | Method | Description | |--------|-------------| | `.failure_policy(policy)` | Per-node failure handling (see [Fault Tolerance](/advanced/circuit-breaker)) | | `.build()` | Finalize and register the node (returns `Result`) | ### Order Guidelines - **0-9**: Critical real-time (motor control, safety) - **10-49**: High priority (sensors, fast control loops) - **50-99**: Normal priority (processing, planning) - **100-199**: Low priority (logging, diagnostics) - **200+**: Background (telemetry, non-essential) ## Global Configuration with Composable Builders Compose the builder methods you need for each deployment stage: ```rust use horus::prelude::*; // Development — lightweight, profiling is always-on let mut scheduler = Scheduler::new() .tick_rate(1000_u64.hz()); // Production — watchdog + blackbox let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()); // Hard real-time — panics without RT capabilities let mut scheduler = Scheduler::new() .require_rt() .tick_rate(1000_u64.hz()); // Safety-critical — require_rt + blackbox + strict deadline misses let mut scheduler = Scheduler::new() .require_rt() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()) .max_deadline_misses(3); ``` ## Python API The Python API uses the unified Node + run() pattern. All scheduling config goes on the Node: ```python from horus import Node, run, us # All config on Node — run() handles the scheduler motor = Node(tick=motor_fn, rate=1000, order=0, budget=300*us, on_miss="skip") planner = Node(tick=planner_fn, order=5) telemetry = Node(tick=telemetry_fn, rate=1, order=10) run(motor, planner, telemetry, rt=True, verbose=True) ``` Or with explicit Scheduler for dynamic control: ```python from horus import Node, Scheduler scheduler = Scheduler(tick_rate=1000, rt=True, cores=[0, 1], verbose=True) scheduler.add(Node(tick=sensor_fn, rate=100, order=0)) scheduler.add(Node(tick=ctrl_fn, rate=100, order=1, on_miss="safe_mode")) scheduler.add(Node(tick=logger_fn, rate=10, order=2, failure_policy="skip")) scheduler.run() ``` Composable config for different deployment stages: ```python from horus import Scheduler # Development — lightweight scheduler = Scheduler() # Production — watchdog + RT scheduler = Scheduler(tick_rate=1000, rt=True, watchdog_ms=500) # With blackbox + telemetry scheduler = Scheduler(tick_rate=1000, watchdog_ms=500, blackbox_mb=64, verbose=True) ``` ## Execution Modes HORUS supports sequential and parallel execution. You configure this through `Scheduler::new()` and per-node execution classes. ### Sequential Mode (Default) Nodes execute one-by-one in priority order — same execution order every tick. Predictable and certification-ready. | Metric | Value | |--------|-------| | Latency | ~100-500ns per node | | Predictable | **Yes** — same order every tick | | Multi-core | No (single thread) | | Best For | Safety-critical, certification | **When to use:** Medical/surgical robots, systems needing reproducible behavior, debugging timing issues, formal verification. ```rust,ignore use horus::prelude::*; // Safety-critical robot controller — 1 kHz tick // rate() auto-marks nodes as RT with derived budget + deadline let mut scheduler = Scheduler::new() .require_rt() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()); scheduler.add(safety_monitor).order(0).rate(1000_u64.hz()).on_miss(Miss::Stop).build()?; scheduler.add(controller).order(1).rate(1000_u64.hz()).on_miss(Miss::SafeMode).build()?; scheduler.run()?; ``` ### Parallel Mode Schedules independent nodes on different CPU cores. Nodes at the same `order` level run concurrently: | Metric | Value | |--------|-------| | Latency | Variable (depends on workload) | | Predictable | Ordering within same priority level varies | | Multi-core | **Yes** | | Best For | Multi-sensor fusion, compute-heavy pipelines | **When to use:** Multi-sensor robots, compute-heavy pipelines, systems with many independent nodes. ```rust,ignore use horus::prelude::*; // Research robot with many sensors — parallel sensor processing let mut scheduler = Scheduler::new(); // These sensor nodes run in parallel (same order number) scheduler.add(lidar_node).order(0).build()?; scheduler.add(camera_node).order(0).build()?; scheduler.add(imu_node).order(0).build()?; // Fusion runs after all sensors (higher order number) scheduler.add(fusion_node).order(1).build()?; scheduler.run()?; ``` ### Mode Comparison | Feature | Sequential | Parallel | |---------|------------|----------| | Predictable Order | **Yes** | Per-priority level | | Multi-core | No | **Yes** | | Best Latency | 87-313ns | Variable | | Certification Ready | **Yes** | No | ## DurationExt and Frequency HORUS provides ergonomic extension methods for creating `Duration` and `Frequency` values, replacing verbose `Duration::from_micros(200)` calls: ### Duration Helpers ```rust use horus::prelude::*; // Microseconds let budget = 200_u64.us(); // Duration::from_micros(200) // Milliseconds let deadline = 1_u64.ms(); // Duration::from_millis(1) // Seconds let timeout = 5_u64.secs(); // Duration::from_secs(5) ``` Works on `u64` literals via the `DurationExt` trait. ### Frequency Type The `.hz()` method creates a `Frequency` that auto-derives timing parameters: ```rust use horus::prelude::*; let freq = 100_u64.hz(); freq.value() // 100.0 Hz freq.period() // 10ms (1/frequency) freq.budget_default() // 8ms (80% of period) freq.deadline_default() // 9.5ms (95% of period) ``` Use `Frequency` with the node builder's `.rate()` method to auto-configure RT timing: ```rust // Auto-derives budget (80% period) and deadline (95% period) // Also auto-marks the node as RT scheduler.add(motor_ctrl) .order(0) .rate(500_u64.hz()) // period=2ms, budget=1.6ms, deadline=1.9ms .on_miss(Miss::Skip) .build()?; ``` | Method | Returns | Description | |--------|---------|-------------| | `.us()` | `Duration` | Microseconds | | `.ms()` | `Duration` | Milliseconds | | `.secs()` | `Duration` | Seconds | | `.hz()` | `Frequency` | Frequency in Hz | | `freq.value()` | `f64` | Frequency in Hz | | `freq.period()` | `Duration` | 1/frequency | | `freq.budget_default()` | `Duration` | 80% of period | | `freq.deadline_default()` | `Duration` | 95% of period | ## See Also - [Scheduling](/concepts/core-concepts-scheduler) - Scheduler overview and node ordering - [Safety Monitor](/advanced/safety-monitor) - Safety monitoring and emergency stop - [Fault Tolerance](/advanced/circuit-breaker) - Failure policies and recovery - [Record & Replay](/advanced/record-replay) - Recording and playback --- ## Safety Monitor Path: /advanced/safety-monitor Description: Real-time safety monitoring with watchdogs, budget enforcement, and deadline miss policies # Safety Monitor The Safety Monitor provides real-time safety monitoring for safety-critical robotics applications. It enforces timing constraints, monitors node health, and applies deadline miss policies when safety violations occur. ## Overview The Safety Monitor includes: - **Watchdogs**: Monitor node liveness — trigger action if a critical node hangs - **Budget Enforcement**: Per-node tick budgets — act if a node takes too long (implicit when nodes have `.rate()` set) - **Deadline Tracking**: Count deadline misses and apply the configured `Miss` policy - **Miss Policies**: `Warn`, `Skip`, `SafeMode`, or `Stop` — per-node control over what happens on deadline miss The Scheduler manages the safety monitor internally — you configure it with composable builder methods and the scheduler automatically feeds watchdogs, checks budgets, and applies miss policies. ## Enabling Safety Monitoring Use composable builder methods to enable safety monitoring. Each method adds a specific safety feature: ```rust use horus::prelude::*; // Production: watchdog for frozen node detection // Budget enforcement is implicit when nodes have .rate() set let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .tick_rate(1000_u64.hz()); // Safety-critical: require RT + blackbox + strict deadline limit let mut scheduler = Scheduler::new() .require_rt() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()) .max_deadline_misses(3); ``` ### Composable Builder Comparison | Builder | Watchdog | Budget Enforcement | Memory Locking | Blackbox | |---------|----------|-------------------|----------------|----------| | `new()` | No | Implicit (when nodes have `.rate()`) | No | No | | `.watchdog(500_u64.ms())` | **Yes** (500ms) | Implicit | No | No | | `.require_rt()` | No | Implicit | **Yes** | No | | `.watchdog(500_u64.ms()).require_rt()` | **Yes** (500ms) | Implicit | **Yes** | No | | `.watchdog(500_u64.ms()).blackbox(64)` | **Yes** (500ms) | Implicit | No | **Yes** (64MB) | ## Configuring Nodes with Rates After configuring the scheduler, add nodes with timing constraints using the node builder. Setting `.rate()` automatically marks the node as RT and derives budget (80% of period) and deadline (95% of period): ```rust use horus::prelude::*; let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .tick_rate(1000_u64.hz()); // RT node — rate auto-derives budget and deadline scheduler.add(motor_controller) .order(0) .rate(1000_u64.hz()) // budget=800us, deadline=950us .on_miss(Miss::SafeMode) // Enter safe state on miss .build()?; scheduler.add(sensor_fusion) .order(1) .rate(200_u64.hz()) // budget=4ms, deadline=4.75ms .on_miss(Miss::Skip) // Skip tick on miss .build()?; scheduler.run()?; ``` ## Watchdogs Watchdogs monitor node liveness. The scheduler automatically feeds watchdogs on successful node ticks. If a critical node fails to execute within the watchdog timeout, the safety monitor triggers graduated degradation. ``` Normal operation: Node tick → success → watchdog fed → timer reset Failure scenario: Node hangs → watchdog timeout expires → graduated degradation → EMERGENCY STOP ``` ### Timeout Guidelines ``` Watchdog timeout should be: - Longer than expected execution time - Shorter than safety-critical response time Example: Expected tick period: 10ms Safety deadline: 100ms Watchdog timeout: 50ms (5× period) ``` ## Budget and Deadline Enforcement Budget and deadline are two levels of timing enforcement: - **Budget** is the expected computation time (soft limit). Budget violations are tracked in `RtStats` for monitoring. - **Deadline** is the hard limit. When exceeded, the `Miss` policy fires (`Warn`, `Skip`, `SafeMode`, or `Stop`). When you set `.rate()`, both are auto-derived: budget = 80% of period, deadline = 95% of period. When you set `.budget()` without `.deadline()`, the deadline equals the budget — your budget IS your hard limit: ```rust // Auto-derived from rate scheduler.add(motor_controller) .order(0) .rate(1000_u64.hz()) // budget=800us, deadline=950us .on_miss(Miss::SafeMode) // Fires on DEADLINE miss (>950us) .build()?; // Explicit budget — deadline auto-derived to match scheduler.add(fast_loop) .order(0) .budget(500_u64.us()) // budget=500us, deadline=500us (auto) .on_miss(Miss::Stop) // Fires when tick exceeds 500us .build()?; // Explicit budget + deadline — slack between them scheduler.add(with_slack) .order(0) .budget(500_u64.us()) // Soft: track violations above 500us .deadline(900_u64.us()) // Hard: Miss policy fires above 900us .on_miss(Miss::SafeMode) .build()?; ``` Violations are also recorded in the BlackBox when using `.blackbox(n)`. ## Node Health States Every node has a health state tracked internally by the scheduler. The four states form a graduated degradation ladder: | State | Meaning | |-------|---------| | `Healthy` | Normal operation — node ticks every cycle | | `Warning` | Watchdog at 1x timeout — node still ticks, but a warning is logged | | `Unhealthy` | Watchdog at 2x timeout — node is **skipped** in the tick loop | | `Isolated` | Watchdog at 3x timeout — `enter_safe_state()` is called, node is skipped | ### Graduated Degradation Transitions The scheduler evaluates watchdog severity every tick and transitions nodes through health states automatically: ``` Healthy ──(1x timeout)──► Warning ──(2x timeout)──► Unhealthy ──(3x timeout)──► Isolated ▲ │ │ │ (successful tick) │ (continued successful ticks restore rate) │ └─────────────────────────┘ │ ▲ │ └──────────────────── (recovery via RestoreRate) ────────────────────────────────┘ ``` **Escalation** happens when a node's watchdog is not fed (the node is slow or hung): - **Healthy to Warning** — 1x watchdog timeout elapsed. The node still runs, but the scheduler logs a warning. - **Warning to Unhealthy** — 2x timeout. The node is skipped entirely in the tick loop to prevent cascading delays. - **Unhealthy to Isolated** — 3x timeout. The scheduler calls `enter_safe_state()` on the node and continues to skip it. For critical nodes, this also triggers an emergency stop. **Recovery** happens on successful ticks: - A `Warning` node that ticks successfully transitions back to `Healthy` immediately, and its watchdog is re-fed. - An `Isolated` or rate-reduced node can recover through the graduated degradation system — after enough consecutive successful ticks at a reduced rate, the scheduler restores the original rate and transitions back to `Healthy`. ### Relationship to Miss Policies Node health states and `Miss` policies are complementary: - **`Miss` policies** act on individual deadline/budget violations (skip one tick, enter safe mode, stop the scheduler). - **Health states** track sustained behavior over time via the watchdog. A node can be in `Warning` even if its `Miss` policy is `Warn` — repeated warnings escalate to `Unhealthy` and eventually `Isolated`. Both systems work together: the `Miss` policy handles immediate responses, while health states provide graduated, automatic degradation for persistently failing nodes. ### Shutdown Report When the scheduler shuts down with `.watchdog()` enabled, the timing report includes a health summary: ``` Node Health: [OK] All 4 nodes healthy ``` Or, if any nodes degraded during the run: ``` Node Health: 3 healthy, 1 warning, 0 unhealthy, 0 isolated, 0 stopped - sensor_fusion: WARNING ``` ## Miss — Deadline Miss Policy The `Miss` enum controls what happens when a node exceeds its deadline: | Policy | Behavior | |--------|----------| | `Miss::Warn` | Log a warning and continue (default) | | `Miss::Skip` | Skip the node for this tick | | `Miss::SafeMode` | Call `enter_safe_state()` on the node | | `Miss::Stop` | Stop the entire scheduler | ### SafeMode in Detail When `Miss::SafeMode` triggers: 1. The scheduler calls `enter_safe_state()` on the offending node 2. Each subsequent tick, the scheduler checks `is_safe_state()` 3. When the node reports safe, normal operation resumes Implement these on your Node: ```rust impl Node for MotorController { fn enter_safe_state(&mut self) { self.velocity = 0.0; self.disable_motor(); } fn is_safe_state(&self) -> bool { self.velocity == 0.0 } fn tick(&mut self) { /* ... */ } } ``` ## RT Node Isolation Each RT node runs on its own dedicated thread by default. If one RT node stalls (deadlock, infinite loop, hardware fault), other RT nodes keep ticking independently on their own threads. ``` Thread 1: [MotorLeft.tick()] → sleep → repeat Thread 2: [MotorRight.tick()] → sleep → repeat ← keeps running Thread 3: [ArmServo.tick()] → sleep → repeat ← keeps running If MotorLeft stalls, MotorRight and ArmServo are unaffected. ``` This is critical for robots where each actuator must be independently controllable. A stalled left wheel controller must not take down the right wheel. Use `.core(N)` to pin specific nodes to CPU cores for cache locality: ```rust scheduler.add(left_motor).order(0).rate(1000_u64.hz()).core(2).build()?; scheduler.add(right_motor).order(1).rate(1000_u64.hz()).core(3).build()?; ``` > **Note:** The watchdog detects stalled nodes but cannot preempt a running `tick()` — cooperative scheduling means the node must return from `tick()` for the watchdog to take action. Thread isolation ensures the stall doesn't cascade to other nodes. ## Shutdown Safety The scheduler guarantees that shutdown always completes, even if an RT node is stalled. Each RT thread gets 3 seconds to exit cleanly after `running` is set to `false`. If a thread doesn't exit within the timeout, it is detached and the scheduler continues shutting down other nodes. This prevents a single stalled node from blocking the entire process — critical for emergency stop scenarios where the robot must halt immediately. ## Emergency Stop Emergency stop is triggered automatically by: - Watchdog expiration (node hangs) - `Miss::Stop` policy on deadline miss - Exceeding the `max_deadline_misses` threshold When emergency stop triggers: 1. All node execution is halted 2. An emergency stop event is recorded in the BlackBox 3. The scheduler transitions to emergency state 4. RT threads are given 3 seconds to exit before being detached ### Inspecting After Emergency Stop ```rust,ignore use horus::prelude::*; let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()); // ... application runs and hits emergency stop ... // Inspect what happened via BlackBox if let Some(bb) = scheduler.get_blackbox() { let anomalies = bb.lock().unwrap().anomalies(); println!("=== SAFETY EVENTS ({}) ===", anomalies.len()); for record in &anomalies { println!("[tick {}] {:?}", record.tick, record.event); } } ``` ## Best Practices ### 1. Start with Conservative Rates Set rates generously initially, then tighten after profiling: ```rust // Start: use rate() — auto-derives budget at 80% of period scheduler.add(motor_controller) .order(0) .rate(500_u64.hz()) // period=2ms, budget=1.6ms .on_miss(Miss::Warn) // Log only while tuning .build()?; // After profiling: tighten to 1kHz scheduler.add(motor_controller) .order(0) .rate(1000_u64.hz()) // period=1ms, budget=800us .on_miss(Miss::SafeMode) // Enforce in production .build()?; ``` ### 2. Layer Safety Checks Use composable builders (watchdog + blackbox) with per-node miss policies: ```rust // .watchdog() gives you frozen node detection // Budget enforcement is implicit from .rate() let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()); // Then set per-node policies for fine-grained control scheduler.add(motor_controller) .order(0) .rate(1000_u64.hz()) .on_miss(Miss::SafeMode) // Critical — enter safe state .build()?; scheduler.add(telemetry) .order(10) .rate(10_u64.hz()) .on_miss(Miss::Skip) // Non-critical — just skip .build()?; ``` ### 3. Choose the Right Configuration | Use Case | Configuration | |----------|--------------| | Medical / surgical robots | `.require_rt().watchdog(500_u64.ms()).blackbox(64)` | | Industrial control | `.require_rt().watchdog(500_u64.ms())` | | CNC / aerospace | `.require_rt().watchdog(500_u64.ms()).blackbox(64).max_deadline_misses(3)` | | General production | `.watchdog(500_u64.ms()).blackbox(64)` | ### 4. Test Safety Setup Verify your system handles deadline misses correctly: ```rust,ignore #[test] fn test_safety_critical_setup() { let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .tick_rate(1000_u64.hz()); scheduler.add(test_node) .order(0) .rate(1000_u64.hz()) .on_miss(Miss::SafeMode) .build() .expect("should build node"); } ``` ## Graduated Watchdog Severity > **Note:** The watchdog and health states are managed automatically by the scheduler — you configure them via `.watchdog(Duration)` and `.on_miss(Miss)` on the node builder. The internal severity levels below explain the scheduler's behavior, not APIs you call directly. The watchdog doesn't just fire a binary "alive/dead" check. It uses **graduated severity** based on how many timeout multiples have elapsed since the last heartbeat: ```text Time since last heartbeat: 0────────1x timeout────────2x timeout────────3x timeout──── │ Ok │ Warning │ Expired │ Critical │ (healthy) │ (node is slow) │ (skip this node) │ (safety response) ``` | Severity | Threshold | Scheduler Response | |----------|-----------|-------------------| | **Ok** | Within timeout | Normal execution | | **Warning** | 1x timeout elapsed | Log warning, node health → `Warning` | | **Expired** | 2x timeout elapsed | Skip node in tick loop, health → `Unhealthy` | | **Critical** | 3x timeout elapsed | Trigger safety response, health → `Isolated` | This prevents a brief jitter from triggering an emergency stop. The scheduler escalates gradually: 1. **Warn** first (gives the node a chance to recover) 2. **Skip** if still unresponsive (other nodes keep running) 3. **Isolate** if critically stuck (enter safe state if configured) ## Tick Timing Ring The scheduler tracks per-node timing statistics using a circular ring buffer: - **Min/Max/Avg** tick execution time per node - Used by the monitor TUI and web dashboard to display CPU load - Helps identify nodes that are close to their budget limits ```rust use horus::prelude::*; // Timing stats are reported in the shutdown summary: // ┌─ Timing Report ─────────────────┐ // │ lidar_driver: avg=0.8ms max=1.2ms budget=2.0ms ✓ // │ planner: avg=4.5ms max=8.1ms budget=5.0ms ⚠ (max exceeds budget) // │ motor_ctrl: avg=0.2ms max=0.3ms budget=1.0ms ✓ // └──────────────────────────────────┘ ``` ## See Also - [Scheduling](/concepts/core-concepts-scheduler) - Scheduler overview and node ordering - [BlackBox Flight Recorder](/advanced/blackbox) - Event recording for post-mortem analysis - [Fault Tolerance](/advanced/circuit-breaker) - Failure policies and recovery - [Scheduler Configuration](/advanced/scheduler-configuration) - Builder methods and node configuration --- ## Linux RT Setup Path: /advanced/rt-setup Description: Configure Linux for real-time scheduling — PREEMPT_RT, permissions, CPU isolation, and Horus RT integration # Linux RT Setup Real-time scheduling requires kernel and permission configuration. This guide walks through every step, from checking your current capabilities to verifying Horus runs with RT scheduling. ## Check Current RT Capabilities Before changing anything, check what your system already supports: ```bash # Check if your kernel has PREEMPT_RT uname -v | grep -i preempt # Check your current RT priority limit (0 means no RT allowed) ulimit -r # Check if SCHED_FIFO works at all chrt -f 1 echo "RT scheduling works" ``` If `ulimit -r` returns `0` or `chrt` fails with "Operation not permitted", follow the sections below. ## Grant RT Permissions Edit `/etc/security/limits.conf` to allow your user (or group) to use RT scheduling: ```bash # Add to /etc/security/limits.conf # Replace 'robotics' with your username or group (@groupname for groups) robotics soft rtprio 99 robotics hard rtprio 99 robotics soft memlock unlimited robotics hard memlock unlimited ``` **Log out and back in** for changes to take effect. Verify with `ulimit -r` — it should now return `99`. ## Install PREEMPT_RT Kernel A standard kernel uses `PREEMPT_VOLUNTARY` or `PREEMPT_DYNAMIC`, which gives millisecond-scale worst-case latency. `PREEMPT_RT` brings that down to microseconds. ### Ubuntu / Debian ```bash sudo apt install linux-image-rt-amd64 # Debian sudo apt install linux-lowlatency # Ubuntu (close to RT) # For full PREEMPT_RT on Ubuntu: sudo apt install linux-image-realtime # Ubuntu Pro / 24.04+ ``` Reboot and select the RT kernel from GRUB. Verify: ```bash uname -v # Should contain "PREEMPT_RT" or "PREEMPT RT" ``` ### From Source (Any Distro) Download the PREEMPT_RT patch from [kernel.org/pub/linux/kernel/projects/rt](https://kernel.org/pub/linux/kernel/projects/rt/), apply it to a matching kernel version, and build with `CONFIG_PREEMPT_RT=y`. ## CPU Isolation Isolate cores from the Linux scheduler so only your RT threads run on them. This eliminates scheduling jitter from other processes. Add `isolcpus` to your kernel command line in `/etc/default/grub`: ```bash # Isolate cores 2 and 3 for RT use GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3" ``` Then `sudo update-grub && sudo reboot`. Verify with: ```bash cat /sys/devices/system/cpu/isolated # Should output: 2-3 ``` Pin Horus nodes to isolated cores: ```rust let mut scheduler = Scheduler::new() .require_rt() .cores(&[2, 3]) .tick_rate(1000_u64.hz()); ``` ## Grant CAP_SYS_NICE As an alternative to `limits.conf`, you can grant the RT capability directly to a binary: ```bash sudo setcap cap_sys_nice=eip ./target/release/my_robot ``` This lets that specific binary use RT scheduling without root or limits.conf changes. Useful for deployment where you do not want blanket RT permissions. ## Verify the Setup ```bash # Run a program with SCHED_FIFO at priority 50 chrt -f 50 ./target/release/my_robot # Check that it is actually running with RT scheduling ps -eo pid,cls,rtprio,comm | grep my_robot # Should show "FF" (FIFO) and priority 50 ``` ## Horus RT Integration ### `.prefer_rt()` vs `.require_rt()` ```rust // Prefer RT: use RT if available, fall back to normal scheduling let mut scheduler = Scheduler::new() .prefer_rt() .tick_rate(500_u64.hz()); // Require RT: panic at startup if RT is not available let mut scheduler = Scheduler::new() .require_rt() .tick_rate(1000_u64.hz()); ``` Use `.prefer_rt()` during development and `.require_rt()` in production when timing guarantees matter. ### Checking Degradations After building the scheduler, inspect whether RT was successfully acquired: ```rust let scheduler = Scheduler::new() .prefer_rt() .tick_rate(500_u64.hz()); // After running, check status (includes any degradations) println!("{}", scheduler.status()); ``` If RT was requested but unavailable, a degradation entry will explain why (missing permissions, no PREEMPT_RT, etc). ## Troubleshooting ### "Operation not permitted" / "cannot set SCHED_FIFO" 1. Check `ulimit -r` — must be > 0 2. Check that limits.conf changes are applied (requires re-login) 3. Try `setcap cap_sys_nice=eip` on the binary 4. If running in Docker: add `--cap-add SYS_NICE` to `docker run` ### RT works but latency is high 1. Verify `PREEMPT_RT` kernel: `uname -v | grep PREEMPT_RT` 2. Check for isolated CPUs: `cat /sys/devices/system/cpu/isolated` 3. Disable CPU frequency scaling: `cpupower frequency-set -g performance` 4. Disable SMT/hyperthreading in BIOS for dedicated RT cores ## Platform-Specific Notes ### NVIDIA Jetson Jetson runs a custom L4T kernel. PREEMPT_RT patches are available from NVIDIA for Jetson Orin and later. Apply them when building the kernel with the Jetson Linux BSP. `isolcpus` works — isolate the performance cores (typically 4-7 on Orin). ### Raspberry Pi Use the `linux-image-rt` package from the Raspberry Pi OS repo, or apply the PREEMPT_RT patch to the `rpi-6.x.y` kernel branch. The Pi 4/5 have 4 cores — isolating cores 2-3 for RT while leaving 0-1 for the OS works well. Set `arm_freq` in `config.txt` to a fixed value to avoid frequency scaling jitter. --- ## Production Deployment Path: /advanced/deployment Description: Deploying HORUS applications to production robots with real-time features, safety monitoring, and performance tuning # Production Deployment Guide to deploying HORUS applications on production robots with real-time constraints, safety monitoring, and optimal performance. ## Deployment Checklist Five steps to go from development to production. ### 1. Enable Safety Monitoring The `.watchdog(Duration)` method enables frozen node detection and creates the safety monitor. Budget enforcement is implicit when nodes have `.rate()` set: ```rust use horus::prelude::*; let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .tick_rate(100_u64.hz()); ``` This is the single most important setting for production. The watchdog detects hung nodes and triggers graduated degradation automatically. ### 2. Configure Real-Time (Optional) HORUS provides two RT modes that enable OS-level `SCHED_FIFO` scheduling and `mlockall()` memory locking: ```rust // Graceful: try RT, warn and continue if unavailable let mut scheduler = Scheduler::new() .prefer_rt() .watchdog(500_u64.ms()) .tick_rate(100_u64.hz()); // Strict: panic if the system lacks RT capabilities let mut scheduler = Scheduler::new() .require_rt() .watchdog(500_u64.ms()) .tick_rate(1000_u64.hz()); ``` **Use `.prefer_rt()` in most cases.** It tries to enable RT features and records any that failed as degradations rather than errors. Use `.require_rt()` only when you need a hard guarantee that RT is active -- it panics if neither `SCHED_FIFO` nor `mlockall` is available. Even without `.prefer_rt()`, the scheduler auto-enables `mlockall` when RT nodes are present and the system permits it. This prevents 10-100 ms page fault spikes under memory pressure. ### 3. Set Node Budgets Assign tick budgets and deadlines to safety-critical nodes. Setting either `.budget()` or `.deadline()` automatically promotes the node to the RT execution class: ```rust scheduler.add(motor_controller) .rate(1000_u64.hz()) .budget(200_u64.us()) // Must finish within 200 us .deadline(800_u64.us()) // Absolute latest: 800 us .on_miss(Miss::SafeMode) // Enter safe state on miss .build()?; scheduler.add(lidar_processor) .rate(20_u64.hz()) .budget(10_u64.ms()) .on_miss(Miss::Skip) // Drop this tick, continue next .build()?; ``` The `Miss` policy controls what happens on deadline miss: | Policy | Behavior | |--------|----------| | `Miss::Warn` | Log a warning and continue (default) | | `Miss::Skip` | Skip the current tick, resume next cycle | | `Miss::SafeMode` | Call `enter_safe_state()` on the node | | `Miss::Stop` | Stop the entire scheduler | ### 4. Enable Flight Recorder The blackbox records scheduler events to a ring buffer on disk for post-mortem analysis: ```rust let mut scheduler = Scheduler::new() .watchdog(500_u64.ms()) .blackbox(64) // 64 MB ring buffer .tick_rate(100_u64.hz()); ``` Data is written to `.horus/blackbox/` in the working directory. See the [Blackbox](/advanced/blackbox) page for analysis tools. ### 5. Require Real-Time For hard real-time deployments, require RT capabilities: ```rust let mut scheduler = Scheduler::new() .require_rt() .watchdog(500_u64.ms()) .blackbox(64) .tick_rate(1000_u64.hz()); ``` This panics if the system lacks `SCHED_FIFO` or `mlockall` capabilities, ensuring your safety-critical system never runs in degraded mode silently. ## RT Kernel Setup (Linux) For hard real-time guarantees, you need a `PREEMPT_RT` kernel. ### Installing the RT Kernel On Ubuntu/Debian: ```bash sudo apt install linux-image-rt-amd64 sudo reboot ``` Verify after reboot: ```bash uname -a # Should show "PREEMPT_RT" in the output ``` ### Setting RT Permissions Add your robot user to the `realtime` group and raise the memlock limit in `/etc/security/limits.conf`: ``` @realtime - rtprio 99 @realtime - memlock unlimited @realtime - nice -20 ``` Then add the user: ```bash sudo groupadd -f realtime sudo usermod -aG realtime robot_user ``` Log out and back in for the changes to take effect. ### Verifying RT Capabilities HORUS prints its RT capability detection at startup. Look for these lines: ``` [SCHEDULER] Memory locked (mlockall) [SCHEDULER] RT scheduling enabled (SCHED_FIFO, priority 50) [SCHEDULER] CPU affinity set to cores [2, 3] ``` If any feature is unavailable with `.prefer_rt()`, it appears as a degradation warning instead of an error. ## Example: Production Robot Configuration A complete production configuration combining all features: ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new() .prefer_rt() .watchdog(500_u64.ms()) .blackbox(64) .max_deadline_misses(5) .tick_rate(1000_u64.hz()); // Safety-critical: motor control at 1 kHz scheduler.add(MotorController::new()) .order(0) .rate(1000_u64.hz()) .on_miss(Miss::SafeMode) .build()?; // Important: state estimation at 200 Hz scheduler.add(StateEstimator::new()) .order(10) .rate(200_u64.hz()) .on_miss(Miss::Warn) .build()?; // Best-effort: telemetry at 10 Hz scheduler.add(TelemetryPublisher::new()) .order(200) .rate(10_u64.hz()) .async_io() .on_miss(Miss::Skip) .build()?; scheduler.run()?; Ok(()) } ``` ## Graceful Degradation When `.prefer_rt()` is used and a feature cannot be applied, it is recorded as a degradation, not an error. The four RT features that can degrade independently are: - **RT Priority** -- `SCHED_FIFO` scheduling - **Memory Locking** -- `mlockall()` to prevent page faults - **CPU Affinity** -- pinning to specific cores - **NUMA Pinning** -- memory locality on multi-socket systems The scheduler continues running with whichever features succeeded. This lets you develop on a laptop (no RT kernel) and deploy on a production system (full RT) without changing code. ## Monitoring in Production ### Safety Statistics Query the safety monitor for budget overruns and deadline misses: ```rust if let Some(stats) = scheduler.safety_stats() { println!("State: {:?}", stats.state()); println!("Budget overruns: {}", stats.budget_overruns()); println!("Deadline misses: {}", stats.deadline_misses()); println!("Watchdog expirations: {}", stats.watchdog_expirations()); } ``` The safety monitor applies graduated degradation automatically: warn, then reduce rate, then isolate the node and call `enter_safe_state()`. ### Node Metrics Get per-node performance data: ```rust for m in scheduler.metrics() { println!("{}: avg={:?}", m.name, m.avg_tick); } ``` ### Blackbox for Post-Mortem After an incident, the blackbox contains a timestamped log of scheduler events including start/stop, budget violations, deadline misses, and watchdog expirations. See [Blackbox](/advanced/blackbox) for details. ## See Also - [Safety Monitor](/advanced/safety-monitor) -- watchdog, budget enforcement, and deadline miss policies - [Scheduler Configuration](/advanced/scheduler-configuration) -- full builder API reference - [BlackBox Flight Recorder](/advanced/blackbox) -- flight recorder and post-mortem analysis ======================================== # SECTION: Standard Library ======================================== --- ## Imu Path: /stdlib/messages/imu Description: Inertial Measurement Unit data — accelerometer, gyroscope, and orientation for sensor fusion and motion estimation # Imu An Inertial Measurement Unit (IMU) message carrying three-axis linear acceleration, three-axis angular velocity, and an optional orientation quaternion. This is the primary message for motion sensing, sensor fusion, and orientation estimation on mobile robots, drones, and manipulators. ## When to Use Use `Imu` when your robot has an IMU sensor (accelerometer + gyroscope) and you need to publish raw or filtered inertial data. Common scenarios include balancing robots, drone flight controllers, dead-reckoning between GPS fixes, and detecting falls or collisions. ## ROS2 Equivalent `sensor_msgs/Imu` -- field layout is identical (orientation quaternion + angular velocity + linear acceleration + covariance matrices). ## Rust Example ```rust use horus::prelude::*; // Create IMU reading from a 6-axis sensor let mut imu = Imu::new(); imu.linear_acceleration = [0.0, 0.0, 9.81]; // m/s^2 (gravity on Z) imu.angular_velocity = [0.0, 0.0, 0.1]; // rad/s (yawing slowly) // Set orientation from Euler angles (roll, pitch, yaw) imu.set_orientation_from_euler(0.0, 0.0, 1.57); // Publish on a topic let topic: Topic = Topic::new("imu.data")?; topic.send(&imu); ``` ## Python Example ```python from horus import Imu, Topic # Create IMU reading (accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z) imu = Imu(0.0, 0.0, 9.81, 0.0, 0.0, 0.1) # Publish on a topic topic = Topic(Imu) topic.send(imu) # Access fields print(f"Accel Z: {imu.accel_z}") print(f"Gyro Z: {imu.gyro_z}") ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `orientation` | `[f64; 4]` | -- | Quaternion `[x, y, z, w]`. Default identity `[0, 0, 0, 1]` | | `orientation_covariance` | `[f64; 9]` | -- | 3x3 row-major covariance. Set `[0]` to `-1` if no orientation data | | `angular_velocity` | `[f64; 3]` | rad/s | Angular velocity `[x, y, z]` | | `angular_velocity_covariance` | `[f64; 9]` | -- | 3x3 row-major covariance matrix | | `linear_acceleration` | `[f64; 3]` | m/s² | Linear acceleration `[x, y, z]` | | `linear_acceleration_covariance` | `[f64; 9]` | -- | 3x3 row-major covariance matrix | | `timestamp_ns` | `u64` | ns | Timestamp in nanoseconds since epoch | ## Methods | Method | Signature | Description | |--------|-----------|-------------| | `new()` | `-> Imu` | Create with identity orientation and current timestamp | | `set_orientation_from_euler` | `(roll, pitch, yaw: f64)` | Set orientation from Euler angles | | `has_orientation()` | `-> bool` | True if `orientation_covariance[0] >= 0` | | `is_valid()` | `-> bool` | True if all values are finite | | `angular_velocity_vec()` | `-> Vector3` | Angular velocity as a `Vector3` | | `linear_acceleration_vec()` | `-> Vector3` | Linear acceleration as a `Vector3` | ## Common Patterns **Sensor fusion pipeline:** ``` IMU hardware --> Imu message --> complementary/Kalman filter --> Pose3D (orientation) \-> dead reckoning --> Odometry (position estimate) ``` **Fall detection:** ```rust use horus::prelude::*; fn check_freefall(imu: &Imu) -> bool { let accel = imu.linear_acceleration_vec(); let magnitude = (accel.x * accel.x + accel.y * accel.y + accel.z * accel.z).sqrt(); magnitude < 1.0 // Near-zero gravity = freefall } ``` **Covariance conventions:** Set `orientation_covariance[0]` to `-1.0` when orientation is not available (e.g., gyro-only sensor). Consumers should call `has_orientation()` before reading the quaternion. --- ## CmdVel Path: /stdlib/messages/cmd-vel Description: Velocity command for mobile robots — linear speed and angular turning rate # CmdVel The most common message type in mobile robotics. Sends a velocity command with forward speed (`linear`) and turning rate (`angular`) to a differential drive, holonomic base, or any mobile platform. ## When to Use Use `CmdVel` whenever you need to command robot motion. This is the standard interface between planners/controllers and drive systems. Every mobile robot recipe uses this type. ## ROS2 Equivalent `geometry_msgs/Twist` (2D subset) — HORUS uses a dedicated 2D type for the common case. For full 3D velocity, use `Twist`. ## Rust Example ```rust use horus::prelude::*; // Command: drive forward at 0.5 m/s, turn left at 0.3 rad/s let cmd = CmdVel { linear: 0.5, angular: 0.3, timestamp_ns: 0 }; let topic: Topic = Topic::new("cmd_vel")?; topic.send(cmd); // SAFETY: always send zero velocity on shutdown topic.send(CmdVel { linear: 0.0, angular: 0.0, timestamp_ns: 0 }); ``` ## Python Example ```python import horus cmd = horus.CmdVel(linear=0.5, angular=0.3) topic = horus.Topic(horus.CmdVel) topic.send(cmd) ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `timestamp_ns` | `u64` | ns | Timestamp (nanoseconds since epoch) | | `linear` | `f32` | m/s | Forward velocity (positive = forward, negative = backward) | | `angular` | `f32` | rad/s | Turning rate (positive = counter-clockwise, negative = clockwise) | ## Safety Notes - **Always send zero on shutdown** — implement `shutdown()` to send `CmdVel { linear: 0.0, angular: 0.0, .. }`. Prevents runaway if the controller crashes. - **Clamp values** — enforce maximum speed/turn rate before sending to hardware. - **Pair with E-stop** — the [Emergency Stop](/recipes/emergency-stop) recipe overrides `cmd_vel` when triggered. ## Differential Drive Kinematics Convert `CmdVel` to left/right wheel speeds: ```rust let left = (cmd.linear - cmd.angular * wheel_base / 2.0) / wheel_radius; let right = (cmd.linear + cmd.angular * wheel_base / 2.0) / wheel_radius; ``` See the [Differential Drive](/recipes/differential-drive) recipe for a complete example. ## Related Types - [Twist](/stdlib/messages/twist) — Full 3D linear + angular velocity - [Odometry](/stdlib/messages/odometry) — Position feedback from wheel encoders - [DifferentialDriveCommand](/rust/api/control-messages) — Direct left/right wheel control --- ## LaserScan Path: /stdlib/messages/laser-scan Description: 2D LiDAR scan data for obstacle detection, mapping, and SLAM # LaserScan A 2D laser scan message representing range measurements from a rotating LiDAR sensor. The scan stores up to 360 range readings in a fixed-size array, making it safe for shared memory transport with zero-copy semantics. ## When to Use Use `LaserScan` when your robot has a 2D LiDAR sensor (e.g., RPLiDAR, Hokuyo, SICK) and you need to publish range data for obstacle avoidance, SLAM, or safety zone monitoring. ## ROS2 Equivalent `sensor_msgs/LaserScan` -- same conceptual fields. HORUS uses a fixed `[f32; 360]` array (shared-memory safe) instead of a dynamic `Vec`. ## Rust Example ```rust use horus::prelude::*; // Create a scan with default parameters (-PI to PI, 1-degree resolution) let mut scan = LaserScan::new(); scan.range_min = 0.1; scan.range_max = 12.0; // Populate ranges from sensor driver scan.ranges[0] = 2.5; // 2.5 meters at angle_min scan.ranges[90] = 1.2; // 1.2 meters at 90 degrees // Query the scan let closest = scan.min_range(); // Nearest valid reading let valid = scan.valid_count(); // Number of valid readings let angle = scan.angle_at(90); // Angle for index 90 // Publish let topic: Topic = Topic::new("lidar.scan")?; topic.send(&scan); ``` ## Python Example ```python from horus import LaserScan, Topic scan = LaserScan( angle_min=-3.14159, angle_max=3.14159, angle_increment=0.01745, range_min=0.1, range_max=12.0, ranges=[1.0, 1.1, 1.2, 0.0, 2.5] # up to 360 values ) topic = Topic(LaserScan) topic.send(scan) # Access fields print(f"Ranges: {scan.ranges[:5]}") print(f"Min range: {scan.range_min}") ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `ranges` | `[f32; 360]` | m | Range measurements. `0.0` = invalid reading | | `angle_min` | `f32` | rad | Start angle. Default: `-PI` | | `angle_max` | `f32` | rad | End angle. Default: `PI` | | `range_min` | `f32` | m | Minimum valid range. Default: `0.1` | | `range_max` | `f32` | m | Maximum valid range. Default: `30.0` | | `angle_increment` | `f32` | rad | Angular resolution. Default: `PI/180` (1 degree) | | `time_increment` | `f32` | s | Time between measurements | | `scan_time` | `f32` | s | Time to complete full scan. Default: `0.1` | | `timestamp_ns` | `u64` | ns | Timestamp in nanoseconds since epoch | ## Methods | Method | Signature | Description | |--------|-----------|-------------| | `new()` | `-> LaserScan` | Create with default parameters and current timestamp | | `angle_at(index)` | `(usize) -> f32` | Angle in radians for a given range index | | `is_range_valid(index)` | `(usize) -> bool` | True if range is within `[range_min, range_max]` and finite | | `valid_count()` | `-> usize` | Number of valid range readings | | `min_range()` | `-> Option` | Minimum valid range reading, or `None` if no valid readings | ## Common Patterns **Typical pipeline:** ``` LiDAR hardware --> LaserScan --> obstacle avoidance --> CmdVel \-> SLAM algorithm --> OccupancyGrid + Pose2D ``` **Simple obstacle avoidance:** ```rust use horus::prelude::*; fn emergency_stop(scan: &LaserScan, safety_distance: f32) -> bool { if let Some(closest) = scan.min_range() { closest < safety_distance } else { true // No valid readings = assume danger } } ``` **Fixed-size array:** The `[f32; 360]` array means `LaserScan` is a POD (Plain Old Data) type that can be sent through shared memory in approximately 50 nanoseconds. If your LiDAR has fewer than 360 beams, leave unused indices at `0.0` (they will be filtered out by `is_range_valid`). If your LiDAR has more than 360 beams, downsample before publishing. --- ## Image Path: /stdlib/messages/image Description: Zero-copy camera images backed by shared memory with ML framework interop # Image A camera image backed by shared memory for zero-copy inter-process communication. Only a small descriptor (metadata) travels through the ring buffer; the actual pixel data stays in a shared memory pool. This enables real-time image pipelines at full camera frame rates without serialization overhead. ## When to Use Use `Image` when your robot has a camera and you need to share frames between nodes -- for example, between a camera driver node, a computer vision node, and a display node. The zero-copy design means a 1080p RGB image transfers in microseconds, not milliseconds. ## ROS2 Equivalent `sensor_msgs/Image` -- same concept (width, height, encoding, pixel data), but HORUS uses shared memory pools instead of serialized byte buffers. ## Zero-Copy Architecture ``` Camera driver Vision node Display node | | | |-- descriptor --> | | | (64 bytes) |-- descriptor --> | | | (64 bytes) | +-----+ +-----+ +-----+ | | | v v v [ Shared Memory Pool -- pixel data lives here ] ``` The descriptor contains pool ID, slot index, dimensions, and encoding. Each recipient maps the same physical memory -- no copies at any stage. ## Encoding Types | Encoding | Channels | Bytes/Pixel | Description | |----------|----------|-------------|-------------| | `Mono8` | 1 | 1 | 8-bit grayscale | | `Mono16` | 1 | 2 | 16-bit grayscale | | `Rgb8` | 3 | 3 | 8-bit RGB (default) | | `Bgr8` | 3 | 3 | 8-bit BGR (OpenCV format) | | `Rgba8` | 4 | 4 | 8-bit RGBA | | `Bgra8` | 4 | 4 | 8-bit BGRA | | `Yuv422` | 2 | 2 | YUV 4:2:2 | | `Mono32F` | 1 | 4 | 32-bit float grayscale | | `Rgb32F` | 3 | 12 | 32-bit float RGB | | `BayerRggb8` | 1 | 1 | Bayer pattern (raw sensor) | | `Depth16` | 1 | 2 | 16-bit depth in millimeters | ## Rust Example ```rust use horus::prelude::*; // Create a 640x480 RGB image (shared memory backed) let mut img = Image::new(640, 480, ImageEncoding::Rgb8)?; img.fill(&[0, 0, 255]); // Fill blue img.set_pixel(100, 200, &[255, 0, 0]); // Red dot at (100, 200) // Send via topic (zero-copy -- only the descriptor travels) let topic: Topic = Topic::new("camera.rgb")?; topic.send(&img); // Receive in another node if let Some(received) = topic.recv() { let px = received.pixel(100, 200); // Zero-copy read let roi = received.roi(0, 0, 320, 240); // Extract region } ``` ## Python Example ```python from horus import Image, Topic # Create a 640x480 RGB image img = Image(480, 640, "rgb8") # Note: Python takes (height, width, encoding) # Create from numpy array (zero-copy into shared memory) import numpy as np frame = np.zeros((480, 640, 3), dtype=np.uint8) img = Image.from_numpy(frame) # Convert to ML frameworks (zero-copy) arr = img.to_numpy() # numpy array t = img.to_torch() # PyTorch tensor j = img.to_jax() # JAX array # Pixel access px = img.pixel(100, 200) img.set_pixel(100, 200, [255, 0, 0]) # Send via topic topic = Topic(Image) topic.send(img) ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `width` | `u32` | px | Image width | | `height` | `u32` | px | Image height | | `channels` | `u32` | -- | Number of color channels | | `encoding` | `ImageEncoding` | -- | Pixel format (see table above) | | `step` | `u32` | bytes | Bytes per row (width * bytes_per_pixel) | | `frame_id` | `str` | -- | Coordinate frame (e.g., `"camera_front"`) | | `timestamp_ns` | `u64` | ns | Timestamp in nanoseconds since epoch | ## Methods | Method | Signature | Description | |--------|-----------|-------------| | `new(w, h, enc)` | `(u32, u32, ImageEncoding) -> Image` | Create zero-initialized image | | `pixel(x, y)` | `(u32, u32) -> Option<&[u8]>` | Read pixel bytes at (x, y) | | `set_pixel(x, y, val)` | `(u32, u32, &[u8]) -> &mut Self` | Write pixel, chainable | | `fill(val)` | `(&[u8]) -> &mut Self` | Fill entire image with color | | `roi(x, y, w, h)` | `(u32, u32, u32, u32) -> Option>` | Extract region of interest | | `data()` | `-> &[u8]` | Raw pixel data slice | | `data_mut()` | `-> &mut [u8]` | Mutable pixel data slice | | `from_numpy(arr)` | Python: array -> Image | Create from numpy (copies in) | | `to_numpy()` | Python: -> ndarray | Zero-copy to numpy | | `to_torch()` | Python: -> Tensor | Zero-copy to PyTorch via DLPack | | `to_jax()` | Python: -> Array | Zero-copy to JAX via DLPack | ## Common Patterns **Camera-to-ML pipeline:** ``` Camera driver --> Image (SHM) --> to_torch() --> YOLO model --> Detection \-> to_numpy() --> OpenCV overlay ``` **Multi-encoding workflow:** ```rust use horus::prelude::*; // Camera outputs BGR (OpenCV convention) let bgr = Image::new(640, 480, ImageEncoding::Bgr8)?; // Depth camera outputs 16-bit depth in millimeters let depth = Image::new(640, 480, ImageEncoding::Depth16)?; // ML model expects float grayscale let gray = Image::new(640, 480, ImageEncoding::Mono32F)?; ``` --- ## Twist Path: /stdlib/messages/twist Description: 3D linear and angular velocity — the full velocity state for robots in 3D space # Twist Represents 3D linear velocity and 3D angular velocity. Used for full 6-DOF velocity representation in odometry, navigation, and force/torque control. For 2D mobile robots, prefer the simpler `CmdVel`. ## When to Use Use `Twist` when you need full 3D velocity: drones, underwater vehicles, manipulator end-effectors, or any system that moves in all six degrees of freedom. Also used as a component of `Odometry` and `TwistWithCovariance`. ## ROS2 Equivalent `geometry_msgs/Twist` — identical field layout (linear `[x, y, z]` + angular `[x, y, z]`). ## Rust Example ```rust use horus::prelude::*; // Drone velocity: 1 m/s forward, 0.5 m/s up, yawing at 0.1 rad/s let twist = Twist { linear: [1.0, 0.0, 0.5], // [x, y, z] m/s angular: [0.0, 0.0, 0.1], // [roll, pitch, yaw] rad/s timestamp_ns: 0, }; let topic: Topic = Topic::new("velocity")?; topic.send(twist); ``` ## Python Example ```python import horus twist = horus.Twist( linear=[1.0, 0.0, 0.5], angular=[0.0, 0.0, 0.1], ) ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `linear` | `[f64; 3]` | m/s | Linear velocity `[x, y, z]` | | `angular` | `[f64; 3]` | rad/s | Angular velocity `[roll, pitch, yaw]` | | `timestamp_ns` | `u64` | ns | Timestamp | ## TwistWithCovariance For uncertainty-aware systems (EKF, navigation stacks), use the covariance variant: ```rust let twist_cov = TwistWithCovariance { twist: Twist { linear: [1.0, 0.0, 0.0], angular: [0.0, 0.0, 0.1], timestamp_ns: 0 }, covariance: [0.0; 36], // 6x6 row-major: [vx, vy, vz, wx, wy, wz] }; ``` | Field | Type | Description | |-------|------|-------------| | `twist` | `Twist` | The velocity | | `covariance` | `[f64; 36]` | 6x6 covariance matrix (row-major) | ## CmdVel vs Twist | Feature | `CmdVel` | `Twist` | |---------|----------|---------| | Dimensions | 2D (linear + angular) | 3D (6-DOF) | | Field types | `f32` | `f64` | | Zero-copy | Yes (`#[repr(C)]`, 12 bytes) | Yes (`#[repr(C)]`, 56 bytes) | | Use case | Mobile robots | Drones, 3D systems | ## Related Types - [CmdVel](/stdlib/messages/cmd-vel) — Simplified 2D velocity command - [Odometry](/stdlib/messages/odometry) — Combines pose + twist for localization - [Accel](/rust/api/geometry-messages) — Linear + angular acceleration --- ## Standard Library Path: /stdlib Description: HORUS standard robotics types — 55+ message types, coordinate transforms, and zero-copy domain types # Standard Library The HORUS Standard Library (`horus_library`) provides **55+ typed messages** for robotics, coordinate transforms (`TransformFrame`), and zero-copy domain types (`Image`, `PointCloud`, `DepthImage`). All types work in both Rust and Python. ## Which Type Do I Need? | I need to... | Use | Rust | Python | |-------------|-----|------|--------| | Drive wheels / send velocity | `CmdVel` | `CmdVel::new(0.5, 0.1)` | `CmdVel(linear=0.5, angular=0.1)` | | Read IMU (accel + gyro) | `Imu` | `imu.linear_acceleration` | `imu.linear_acceleration` | | Read LiDAR scan | `LaserScan` | `scan.ranges` | `scan.ranges` | | Send camera images | `Image` | `Image::new(640, 480, Rgb8)` | `Image(640, 480, encoding=0)` | | Send 3D point cloud | `PointCloud` | `PointCloud::new(1000, XYZ)` | `PointCloud(num_points=1000)` | | Read wheel odometry | `Odometry` | `odom.x`, `odom.y` | `odom.x`, `odom.y` | | Control joints | `JointState` | `js.positions` | `js.positions` | | Detect objects (2D) | `Detection` | `det.class_name` | `det.class_name` | | Detect objects (3D) | `Detection3D` | `det.bbox` | `det.bbox` | | Build a 2D map | `OccupancyGrid` | `grid.is_free(x, y)` | `grid.is_free(x, y)` | | Plan a path | `NavPath` | `path.add_waypoint(wp)` | `path.add_waypoint(wp)` | | Estimate body pose | `LandmarkArray` | `.coco_pose()` | `.coco_pose()` | | Transform coordinates | `TransformFrame` | `tf.lookup("camera", "world")` | `tf.lookup("camera", "world")` | | Monitor robot health | `SafetyStatus` | `status.estop_engaged` | `status.estop_engaged` | ## Sections - **[Message Types](/stdlib/messages)** — All 55+ message types organized by category - **[TransformFrame](/concepts/transform-frame)** — Coordinate transform system - **[Image](/rust/api/image)** — Zero-copy camera images - **[PointCloud](/rust/api/perception-messages)** — 3D point cloud data - **[DepthImage](/rust/api/depth-image)** — Depth maps from stereo/structured light - **[Tensor & DLPack](/rust/api/tensor)** — ML tensor exchange with PyTorch/JAX - **[Python Message Library](/python/library/python-message-library)** — All types in Python with field tables --- ## OccupancyGrid & CostMap Path: /stdlib/messages/occupancy-grid Description: Grid-based environment maps for navigation, mapping, and path planning # OccupancyGrid & CostMap `OccupancyGrid` represents a 2D grid map of the environment where each cell stores an occupancy probability. `CostMap` extends it with inflated costs around obstacles for safe path planning. Together they form the mapping and planning backbone for autonomous navigation. ## When to Use Use `OccupancyGrid` when your robot builds maps from sensor data (SLAM) or loads pre-built maps for localization. Use `CostMap` when you need to plan paths that keep a safe distance from obstacles. ## ROS2 Equivalent `nav_msgs/OccupancyGrid` -- same grid structure with origin, resolution, and cell values. HORUS adds `CostMap` as a first-class type (in ROS2 this lives in the costmap_2d package). ## Cell Values | Value | Meaning | |-------|---------| | `-1` | Unknown (unexplored) | | `0` | Free space | | `1-49` | Probably free (low probability of obstacle) | | `50-99` | Probably occupied | | `100` | Definitely occupied | ## Rust Example ```rust use horus::prelude::*; // Create a 10m x 10m map at 5cm resolution let origin = Pose2D::new(-5.0, -5.0, 0.0); let mut grid = OccupancyGrid::new(200, 200, 0.05, origin); // Mark a cell as occupied grid.set_occupancy(100, 100, 100); // Convert between world and grid coordinates let (gx, gy) = grid.world_to_grid(1.5, 2.0).unwrap(); let (wx, wy) = grid.grid_to_world(gx, gy).unwrap(); // Query occupancy if grid.is_free(1.5, 2.0) { println!("Path is clear"); } // Create a cost map for path planning let costmap = CostMap::from_occupancy_grid(grid, 0.55); // 55cm inflation let cost = costmap.cost(1.5, 2.0); ``` ## Python Example ```python from horus import OccupancyGrid, Topic # Create a 100x100 grid at 5cm resolution grid = OccupancyGrid(100, 100, 0.05) # Set cell values grid.set_occupancy(50, 50, 100) # Mark as occupied # Query the map if grid.is_free(1.0, 1.0): print("Cell is free") # Coordinate conversion gx, gy = grid.world_to_grid(1.0, 1.0) wx, wy = grid.grid_to_world(gx, gy) # Publish topic = Topic(OccupancyGrid) topic.send(grid) ``` ## OccupancyGrid Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `resolution` | `f32` | m/cell | Meters per cell. Default: `0.05` (5cm) | | `width` | `u32` | cells | Grid width | | `height` | `u32` | cells | Grid height | | `origin` | `Pose2D` | m, rad | World pose of the bottom-left corner | | `data` | `Vec` | 0--100 | Cell values, row-major. `-1`=unknown, `0`=free, `100`=occupied | | `frame_id` | `[u8; 32]` | -- | Coordinate frame (e.g., `"map"`) | | `metadata` | `[u8; 64]` | -- | Free-form metadata | | `timestamp_ns` | `u64` | ns | Timestamp in nanoseconds since epoch | ## OccupancyGrid Methods | Method | Signature | Description | |--------|-----------|-------------| | `new(w, h, res, origin)` | `(u32, u32, f32, Pose2D) -> Self` | Create grid initialized to unknown (-1) | | `world_to_grid(x, y)` | `(f64, f64) -> Option<(u32, u32)>` | Convert world coordinates to grid indices | | `grid_to_world(gx, gy)` | `(u32, u32) -> Option<(f64, f64)>` | Convert grid indices to world coordinates (cell center) | | `occupancy(gx, gy)` | `(u32, u32) -> Option` | Get cell value at grid coordinates | | `set_occupancy(gx, gy, val)` | `(u32, u32, i8) -> bool` | Set cell value (clamped to -1..100) | | `is_free(x, y)` | `(f64, f64) -> bool` | True if occupancy is in 0..50 | | `is_occupied(x, y)` | `(f64, f64) -> bool` | True if occupancy >= 50 | ## CostMap Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `occupancy_grid` | `OccupancyGrid` | -- | Underlying occupancy grid | | `costs` | `Vec` | 0--255 | Cost values per cell. `253`=lethal, `255`=unknown | | `inflation_radius` | `f32` | m | Inflation radius. Default: `0.55` | | `cost_scaling_factor` | `f32` | -- | Exponential decay factor. Default: `10.0` | | `lethal_cost` | `u8` | -- | Cost threshold for lethal obstacles. Default: `253` | ## CostMap Methods | Method | Signature | Description | |--------|-----------|-------------| | `from_occupancy_grid(grid, radius)` | `(OccupancyGrid, f32) -> Self` | Create costmap with inflation | | `compute_costs()` | `-> ()` | Recompute costs from occupancy data | | `cost(x, y)` | `(f64, f64) -> Option` | Get cost at world coordinates | ## Common Patterns **SLAM pipeline:** ``` LaserScan --> SLAM algorithm --> OccupancyGrid (map) \-> Pose2D (localization) ``` **Path planning pipeline:** ``` OccupancyGrid --> CostMap (inflated) --> path planner --> NavPath \-> CmdVel ``` **Inflation:** The `CostMap` applies exponential cost decay around obstacles. A cell at distance `d` from an obstacle gets cost proportional to `(1 - d/radius)^scaling_factor`. This creates a smooth gradient that path planners use to keep the robot away from walls and obstacles. --- ## Odometry Path: /stdlib/messages/odometry Description: Robot position and velocity from wheel encoders or visual odometry # Odometry Combines a 2D pose (position + heading) with a twist (velocity) and covariance. The standard output from wheel encoders, visual odometry, or any localization system. ## When to Use Use `Odometry` when you need to publish or consume the robot's estimated position and velocity. Typically published by a drive node that integrates wheel encoder counts, and consumed by planners, fusion nodes, or logging systems. ## ROS2 Equivalent `nav_msgs/Odometry` — similar structure (pose + twist + covariances). ## Rust Example ```rust use horus::prelude::*; // Robot at (1.5, 2.0) heading 45 degrees, moving forward at 0.3 m/s let odom = Odometry { pose: Pose2D { x: 1.5, y: 2.0, theta: 0.785 }, twist: Twist { linear: [0.3, 0.0, 0.0], angular: [0.0, 0.0, 0.05], timestamp_ns: 0, }, pose_covariance: [0.0; 36], twist_covariance: [0.0; 36], timestamp_ns: 0, }; let topic: Topic = Topic::new("odom")?; topic.send(odom); ``` ## Python Example ```python import horus odom = horus.Odometry( pose=horus.Pose2D(x=1.5, y=2.0, theta=0.785), twist=horus.Twist(linear=[0.3, 0.0, 0.0], angular=[0.0, 0.0, 0.05]), ) ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `pose` | `Pose2D` | m, rad | Current position and heading estimate | | `twist` | `Twist` | m/s, rad/s | Current velocity estimate | | `pose_covariance` | `[f64; 36]` | -- | 6x6 covariance for pose (x, y, z, roll, pitch, yaw) | | `twist_covariance` | `[f64; 36]` | -- | 6x6 covariance for velocity | | `timestamp_ns` | `u64` | ns | Timestamp | ## Common Patterns ### Publish from Drive Node ```rust fn tick(&mut self) { // Read wheel encoders let left_ticks = self.read_left_encoder(); let right_ticks = self.read_right_encoder(); // Integrate to get pose self.x += dx * self.theta.cos(); self.y += dx * self.theta.sin(); self.theta += dtheta; self.odom_pub.send(Odometry { pose: Pose2D { x: self.x, y: self.y, theta: self.theta }, twist: Twist { linear: [self.speed, 0.0, 0.0], angular: [0.0, 0.0, self.omega], timestamp_ns: 0, }, pose_covariance: [0.0; 36], twist_covariance: [0.0; 36], timestamp_ns: 0, }); } ``` ### Fuse with IMU See [Multi-Sensor Fusion](/recipes/multi-sensor-fusion) — combines odometry heading with IMU yaw using a complementary filter. ## Related Types - [Pose2D](/stdlib/messages/pose) — 2D position and heading - [Twist](/stdlib/messages/twist) — 3D velocity - [CmdVel](/stdlib/messages/cmd-vel) — Velocity commands (input to drive) --- ## Detection & Detection3D Path: /stdlib/messages/detection Description: ML object detection results for 2D bounding boxes and 3D oriented bounding boxes # Detection & Detection3D Fixed-size object detection messages for zero-copy IPC. `Detection` holds a 2D bounding box result from models like YOLO or SSD. `Detection3D` holds a 3D oriented bounding box from point cloud detectors or depth-aware models. Both are fixed-size types — transferred via zero-copy shared memory (72 and 104 bytes respectively). ## When to Use Use `Detection` when your robot runs a 2D object detection model on camera images and needs to publish results to downstream nodes (tracking, planning, visualization). Use `Detection3D` when you have 3D detections from LiDAR-based or depth-aware models. ## ROS2 Equivalent - `Detection` maps to `vision_msgs/Detection2D` - `Detection3D` maps to `vision_msgs/Detection3D` ## Rust Example ```rust use horus::prelude::*; // 2D detection from YOLO let det = Detection::new("person", 0.92, 100.0, 50.0, 200.0, 400.0); // class conf x y width height // Check confidence threshold if det.is_confident(0.5) { println!("Found {} at ({}, {})", det.class_name(), det.bbox.x, det.bbox.y); } // 3D detection from point cloud let bbox3d = BoundingBox3D::new(5.0, 2.0, 0.5, 4.5, 2.0, 1.5, 0.1); // cx cy cz len wid hgt yaw let det3d = Detection3D::new("car", 0.88, bbox3d) .with_velocity(10.0, 5.0, 0.0); // Publish detections let topic: Topic = Topic::new("detections.2d")?; topic.send(&det); ``` ## Python Example ```python from horus import Detection, Topic # Create a detection det = Detection("person", 0.92, 100.0, 50.0, 200.0, 400.0) # class conf x y width height # Access properties print(f"Class: {det.class_name}, Confidence: {det.confidence}") print(f"BBox: ({det.bbox.x}, {det.bbox.y}, {det.bbox.width}, {det.bbox.height})") print(f"Area: {det.bbox.area}") # Confidence filtering if det.is_confident(0.5): topic = Topic(Detection) topic.send(det) ``` ## Detection Fields | Field | Type | Unit | Size | Description | |-------|------|------|------|-------------| | `bbox` | `BoundingBox2D` | px | 16 B | Bounding box `(x, y, width, height)` | | `confidence` | `f32` | 0--1 | 4 B | Detection confidence | | `class_id` | `u32` | -- | 4 B | Numeric class identifier | | `class_name` | `[u8; 32]` | -- | 32 B | UTF-8 class label, null-padded (max 31 chars) | | `instance_id` | `u32` | -- | 4 B | Instance ID for instance segmentation | **Total size: 72 bytes (fixed-size, zero-copy)** ## Detection Methods | Method | Signature | Description | |--------|-----------|-------------| | `new(name, conf, x, y, w, h)` | `(&str, f32, f32, f32, f32, f32) -> Self` | Create with class name and bounding box | | `with_class_id(id, conf, bbox)` | `(u32, f32, BoundingBox2D) -> Self` | Create with numeric class ID | | `class_name()` | `-> &str` | Get class name as string | | `set_class_name(name)` | `(&str) -> ()` | Set class name (truncates to 31 chars) | | `is_confident(threshold)` | `(f32) -> bool` | True if confidence >= threshold | ## BoundingBox2D Methods | Method | Signature | Description | |--------|-----------|-------------| | `new(x, y, w, h)` | `(f32, f32, f32, f32) -> Self` | Create from top-left corner | | `from_center(cx, cy, w, h)` | `(f32, f32, f32, f32) -> Self` | Create from center (YOLO format) | | `center_x()`, `center_y()` | `-> f32` | Center coordinates | | `area()` | `-> f32` | Box area in pixels | | `iou(other)` | `(&BoundingBox2D) -> f32` | Intersection over Union | ## Detection3D Fields | Field | Type | Unit | Size | Description | |-------|------|------|------|-------------| | `bbox` | `BoundingBox3D` | m, rad | 48 B | 3D box: center, dimensions, rotation (roll/pitch/yaw) | | `confidence` | `f32` | 0--1 | 4 B | Detection confidence | | `class_id` | `u32` | -- | 4 B | Numeric class identifier | | `class_name` | `[u8; 32]` | -- | 32 B | UTF-8 class label | | `velocity_x/y/z` | `f32` | m/s | 12 B | Object velocity (for tracking-enabled detectors) | | `instance_id` | `u32` | -- | 4 B | Tracking/instance ID | **Total size: 104 bytes (fixed-size, zero-copy)** ## Common Patterns **Camera-to-tracking pipeline:** ``` Camera --> Image --> YOLO model --> Detection --> tracker --> TrackedObject \-> filter by confidence \-> filter by class ``` **Confidence filtering pattern:** ```rust use horus::prelude::*; fn filter_detections(detections: &[Detection], min_conf: f32) -> Vec<&Detection> { detections.iter() .filter(|d| d.is_confident(min_conf)) .collect() } ``` **NMS (Non-Maximum Suppression):** ```rust use horus::prelude::*; fn nms(dets: &mut Vec, iou_threshold: f32) { dets.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap()); let mut keep = vec![true; dets.len()]; for i in 0..dets.len() { if !keep[i] { continue; } for j in (i + 1)..dets.len() { if keep[j] && dets[i].bbox.iou(&dets[j].bbox) > iou_threshold { keep[j] = false; } } } let mut idx = 0; dets.retain(|_| { let k = keep[idx]; idx += 1; k }); } ``` --- ## Pose2D / Pose3D Path: /stdlib/messages/pose Description: 2D and 3D position and orientation types for robotics # Pose2D / Pose3D Position and orientation in 2D or 3D space. The fundamental geometry types for localization, navigation, and manipulation. ## Pose2D — 2D Position + Heading The standard representation for ground robots: position (x, y) and heading angle (theta). ### Rust ```rust use horus::prelude::*; let pose = Pose2D { x: 1.5, y: 2.0, theta: 0.785 }; // 45 degrees ``` ### Python ```python import horus pose = horus.Pose2D(x=1.5, y=2.0, theta=0.785) ``` ### Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `x` | `f64` | m | X position | | `y` | `f64` | m | Y position | | `theta` | `f64` | rad | Heading angle (0 = forward, positive = counter-clockwise) | --- ## Pose3D — 3D Position + Quaternion Full 6-DOF pose for 3D applications: drones, manipulator end-effectors, VR tracking. ### Rust ```rust use horus::prelude::*; let pose = Pose3D { position: Point3 { x: 1.0, y: 2.0, z: 0.5 }, orientation: Quaternion { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }, // identity timestamp_ns: 0, }; ``` ### Python ```python import horus pose = horus.Pose3D( position=horus.Point3(x=1.0, y=2.0, z=0.5), orientation=horus.Quaternion(x=0.0, y=0.0, z=0.0, w=1.0), ) ``` ### Fields | Field | Type | Description | |-------|------|-------------| | `position` | `Point3` | 3D position `{ x, y, z }` in meters | | `orientation` | `Quaternion` | Orientation as `{ x, y, z, w }` quaternion | | `timestamp_ns` | `u64` | Timestamp | --- ## PoseStamped — Pose3D with Frame ID Adds a coordinate frame identifier for use with the [Transform Frame](/concepts/transform-frame) system. ```rust let stamped = PoseStamped { pose: Pose3D { position: Point3 { x: 1.0, y: 0.0, z: 0.0 }, orientation: Quaternion { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }, timestamp_ns: 0, }, frame_id: *b"base_link\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", timestamp_ns: 0, }; ``` ### Fields | Field | Type | Description | |-------|------|-------------| | `pose` | `Pose3D` | The 3D pose | | `frame_id` | `[u8; 32]` | Coordinate frame (null-terminated string, max 31 chars) | | `timestamp_ns` | `u64` | Timestamp | --- ## PoseWithCovariance — Uncertainty-Aware Pose For EKF and probabilistic localization. The 6x6 covariance matrix represents uncertainty in `[x, y, z, roll, pitch, yaw]`. ```rust let pose_cov = PoseWithCovariance { pose: Pose3D { position: Point3 { x: 1.0, y: 2.0, z: 0.0 }, orientation: Quaternion { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }, timestamp_ns: 0, }, covariance: [0.0; 36], // 6x6 row-major }; ``` ### Fields | Field | Type | Description | |-------|------|-------------| | `pose` | `Pose3D` | The 3D pose | | `covariance` | `[f64; 36]` | 6x6 covariance matrix (row-major) | --- ## Choosing the Right Type | Type | Dimensions | Use Case | |------|-----------|----------| | `Pose2D` | x, y, theta | Ground robots, 2D navigation | | `Pose3D` | xyz + quaternion | Drones, arms, 3D perception | | `PoseStamped` | Pose3D + frame | Transform frame integration | | `PoseWithCovariance` | Pose3D + covariance | EKF, probabilistic localization | ## Related Types - [Odometry](/stdlib/messages/odometry) — Pose + velocity from wheel encoders - [Twist](/stdlib/messages/twist) — Velocity (the derivative of pose) - [TransformStamped](/concepts/transform-frame) — Relative transform between frames - [Point3, Vector3, Quaternion](/rust/api/geometry-messages) — Primitive geometry types --- ## SegmentationMask Path: /stdlib/messages/segmentation Description: Pixel-level segmentation masks for semantic, instance, and panoptic segmentation # SegmentationMask A fixed-size header (64 bytes, zero-copy transport) describing a pixel-level segmentation mask. The mask data follows the header as a raw byte array where each pixel stores a class ID (semantic), instance ID (instance), or both (panoptic). Three static constructors select the segmentation mode. ## When to Use Use `SegmentationMask` when your robot runs a segmentation model and needs to label every pixel in an image. Common scenarios include driveable surface detection (semantic), grasping individual objects (instance), and full scene understanding (panoptic). ## ROS2 Equivalent No direct ROS2 equivalent. ROS2 typically publishes segmentation as `sensor_msgs/Image` with class IDs encoded as pixel values. HORUS provides a dedicated type with mode metadata. ## Three Segmentation Modes | Mode | Value | Pixel Meaning | Use Case | |------|-------|---------------|----------| | **Semantic** | `0` | Class ID (0-255). Each class gets one color. | "What is this pixel?" -- road, sidewalk, sky | | **Instance** | `1` | Instance ID (0-255). Each object gets a unique ID. | "Which object is this pixel?" -- person #1, person #2 | | **Panoptic** | `2` | Both class and instance encoded. | "What and which?" -- car #3, tree #7 | ## Rust Example ```rust use horus::prelude::*; // Semantic segmentation: 80 COCO classes let mask = SegmentationMask::semantic(640, 480, 80) .with_frame_id("camera_front"); // Instance segmentation: no class count needed let mask = SegmentationMask::instance(640, 480); // Panoptic segmentation: class + instance let mask = SegmentationMask::panoptic(640, 480, 80); // Query mask type assert!(mask.is_panoptic()); assert!(!mask.is_semantic()); // Calculate data buffer size let buf_size = mask.data_size(); // width * height bytes (u8 per pixel) ``` ## Python Example ```python from horus import SegmentationMask, Topic # Semantic segmentation (mask_type=0) mask = SegmentationMask.semantic(640, 480, 80) # Instance segmentation (mask_type=1) mask = SegmentationMask.instance(640, 480) # Panoptic segmentation (mask_type=2) mask = SegmentationMask.panoptic(640, 480, 80) # Or use the constructor with mask_type parameter mask = SegmentationMask(640, 480, mask_type=0, num_classes=80) # Check type print(f"Is semantic: {mask.is_semantic()}") print(f"Dimensions: {mask.width}x{mask.height}") print(f"Classes: {mask.num_classes}") # Publish topic = Topic(SegmentationMask) topic.send(mask) ``` ## Fields | Field | Type | Unit | Size | Description | |-------|------|------|------|-------------| | `width` | `u32` | px | 4 B | Image width | | `height` | `u32` | px | 4 B | Image height | | `num_classes` | `u32` | -- | 4 B | Number of classes (semantic/panoptic). `0` for instance mode | | `mask_type` | `u32` | -- | 4 B | `0`=semantic, `1`=instance, `2`=panoptic | | `timestamp_ns` | `u64` | ns | 8 B | Timestamp in nanoseconds since epoch | | `seq` | `u64` | -- | 8 B | Sequence number | | `frame_id` | `[u8; 32]` | -- | 32 B | Coordinate frame (e.g., `"camera_front"`) | **Total header size: 64 bytes (fixed-size, zero-copy)** ## Methods | Method | Signature | Description | |--------|-----------|-------------| | `semantic(w, h, classes)` | `(u32, u32, u32) -> Self` | Create semantic mask header | | `instance(w, h)` | `(u32, u32) -> Self` | Create instance mask header | | `panoptic(w, h, classes)` | `(u32, u32, u32) -> Self` | Create panoptic mask header | | `is_semantic()` | `-> bool` | True if `mask_type == 0` | | `is_instance()` | `-> bool` | True if `mask_type == 1` | | `is_panoptic()` | `-> bool` | True if `mask_type == 2` | | `data_size()` | `-> usize` | Buffer size for u8 mask (`width * height`) | | `data_size_u16()` | `-> usize` | Buffer size for u16 mask (`width * height * 2`) | | `with_frame_id(id)` | `(&str) -> Self` | Set coordinate frame, chainable | | `with_timestamp(ts)` | `(u64) -> Self` | Set timestamp, chainable | | `frame_id()` | `-> &str` | Get frame ID as string | ## COCO Class Constants The `segmentation::classes` module provides standard COCO class IDs: ```rust use horus::prelude::*; use horus::messages::segmentation::classes; let is_person = pixel_class == classes::PERSON; // 1 let is_car = pixel_class == classes::CAR; // 3 let is_dog = pixel_class == classes::DOG; // 18 let is_background = pixel_class == classes::BACKGROUND; // 0 ``` ## Common Patterns **Segmentation pipeline:** ``` Camera --> Image --> segmentation model --> SegmentationMask \-> overlay on Image for visualization ``` **Driveable surface detection:** ```rust use horus::prelude::*; fn driveable_area(mask_data: &[u8], width: u32, road_class: u8) -> f32 { let total = mask_data.len() as f32; let road_pixels = mask_data.iter().filter(|&&p| p == road_class).count() as f32; road_pixels / total } ``` **Instance counting:** ```rust use horus::prelude::*; fn count_instances(mask_data: &[u8]) -> usize { let mut seen = [false; 256]; for &id in mask_data { if id > 0 { // Skip background seen[id as usize] = true; } } seen.iter().filter(|&&v| v).count() } ``` **Panoptic encoding:** In panoptic mode, use u16 masks (`data_size_u16()`) to encode both class and instance: `encoded = class_id * 256 + instance_id`. This supports up to 256 classes with up to 256 instances each. --- ## BatteryState Path: /stdlib/messages/battery-state Description: Battery monitoring — voltage, current, temperature, cell voltages, and charge status # BatteryState Battery health and charge state for any battery-powered robot. Reports voltage, current draw, temperature, individual cell voltages, and charge percentage. ## When to Use Use `BatteryState` when your robot runs on batteries and you need to monitor power levels, trigger low-battery warnings, or initiate safe shutdown. Essential for mobile robots, drones, and any untethered system. ## ROS2 Equivalent `sensor_msgs/BatteryState` — similar structure (voltage, current, charge, capacity, temperature, cell voltages). ## Rust Example ```rust use horus::prelude::*; let battery = BatteryState { voltage: 12.6, current: -2.5, // negative = discharging charge: f32::NAN, // NaN if unknown capacity: f32::NAN, percentage: 85.0, // 85% charge power_supply_status: 2, // discharging temperature: 32.0, // Celsius cell_voltages: [0.0; 16], cell_count: 0, timestamp_ns: 0, }; let topic: Topic = Topic::new("battery.state")?; topic.send(battery); ``` ## Python Example ```python import horus battery = horus.BatteryState( voltage=12.6, current=-2.5, percentage=85.0, temperature=32.0, ) ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `voltage` | `f32` | V | Total pack voltage | | `current` | `f32` | A | Current draw (negative = discharging) | | `charge` | `f32` | Ah | Remaining charge (`NaN` if unknown) | | `capacity` | `f32` | Ah | Full capacity (`NaN` if unknown) | | `percentage` | `f32` | % | State of charge (0–100) | | `power_supply_status` | `u8` | — | 0=unknown, 1=charging, 2=discharging, 3=full | | `temperature` | `f32` | °C | Pack temperature | | `cell_voltages` | `[f32; 16]` | V | Per-cell voltages (if available) | | `cell_count` | `u8` | — | Number of valid cell readings | | `timestamp_ns` | `u64` | ns | Timestamp | ## Common Patterns ### Low Battery Warning ```rust fn tick(&mut self) { // IMPORTANT: always recv() every tick if let Some(battery) = self.battery_sub.recv() { if battery.percentage < 20.0 { hlog!(warn, "Low battery: {:.0}% ({:.1}V)", battery.percentage, battery.voltage); } if battery.percentage < 5.0 { // SAFETY: trigger safe shutdown hlog!(error, "Critical battery: {:.0}% — shutting down", battery.percentage); self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0, timestamp_ns: 0 }); } } } ``` ## Related Types - [EmergencyStop](/rust/api/diagnostics-messages) — Triggered by critical battery - [DiagnosticStatus](/rust/api/diagnostics-messages) — General health reporting - [ResourceUsage](/rust/api/diagnostics-messages) — CPU, memory, and system stats --- ## Navigation Messages Path: /stdlib/messages/navigation Description: NavGoal, NavPath, PathPlan, and CostMap — the navigation stack interface # Navigation Messages Messages for goal-based navigation: send a target pose, receive a planned path, and follow waypoints. These form the interface between high-level commands ("go to the kitchen") and low-level control ([CmdVel](/stdlib/messages/cmd-vel)). ## NavGoal — Where to Go A navigation target with position, orientation, and tolerances. ### Rust ```rust use horus::prelude::*; let goal = NavGoal { target_pose: Pose2D { x: 5.0, y: 3.0, theta: 0.0 }, tolerance_position: 0.1, // 10cm position tolerance tolerance_angle: 0.05, // ~3° heading tolerance timeout_seconds: 30.0, // 30s to reach goal (0 = no limit) timestamp_ns: 0, }; let topic: Topic = Topic::new("nav.goal")?; topic.send(goal); ``` ### Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `target_pose` | `Pose2D` | m, rad | Target position and heading | | `tolerance_position` | `f64` | m | Acceptable position error | | `tolerance_angle` | `f64` | rad | Acceptable heading error | | `timeout_seconds` | `f64` | s | Max time to reach goal (0 = unlimited) | | `timestamp_ns` | `u64` | ns | Timestamp | --- ## NavPath — Planned Waypoints A sequence of waypoints computed by a path planner. Each waypoint has position, heading, and speed. ### Rust ```rust use horus::prelude::*; // Subscribe to planned paths from the planner node let path_sub: Topic = Topic::new("nav.path")?; if let Some(path) = path_sub.recv() { for i in 0..path.waypoint_count as usize { let wp = &path.waypoints[i]; // Follow waypoint: wp.x, wp.y, wp.theta, wp.velocity } } ``` ### Key Fields | Field | Type | Description | |-------|------|-------------| | `waypoints` | `[Waypoint; 256]` | Array of waypoints (max 256) | | `waypoint_count` | `u16` | Number of valid waypoints | | `total_length` | `f64` | Total path length in meters | | `estimated_time` | `f64` | Estimated completion time in seconds | --- ## PathPlan — Compact Path Representation A flat-array path format optimized for zero-copy IPC. Stores waypoints as packed `[x, y, theta]` triples. ### Rust ```rust use horus::prelude::*; let plan_sub: Topic = Topic::new("nav.plan")?; if let Some(plan) = plan_sub.recv() { let n = plan.waypoint_count as usize; for i in 0..n { let x = plan.waypoint_data[i * 3]; let y = plan.waypoint_data[i * 3 + 1]; let theta = plan.waypoint_data[i * 3 + 2]; } } ``` ### Key Fields | Field | Type | Description | |-------|------|-------------| | `waypoint_data` | `[f32; 768]` | Packed `[x, y, theta]` × 256 waypoints | | `goal_pose` | `[f32; 3]` | Target `[x, y, theta]` | | `waypoint_count` | `u16` | Number of valid waypoints | --- ## CostMap — Navigation Cost Grid An inflated cost grid built on top of an [OccupancyGrid](/stdlib/messages/occupancy-grid). Used by planners to avoid obstacles with safety margins. ### Key Fields | Field | Type | Description | |-------|------|-------------| | `occupancy_grid` | `OccupancyGrid` | Base grid data | | `costs` | `Vec` | Cell costs (0=free, 254=inscribed, 255=lethal) | | `inflation_radius` | `f32` | Obstacle inflation radius in meters | | `cost_scaling_factor` | `f32` | Exponential cost decay factor | --- ## Navigation Pipeline ```text User/Planner → NavGoal → Path Planner → NavPath/PathPlan → Path Follower → CmdVel → Drive ↑ CostMap (from OccupancyGrid + LiDAR) ``` ## Related Types - [CmdVel](/stdlib/messages/cmd-vel) — Velocity commands (output of path follower) - [Odometry](/stdlib/messages/odometry) — Robot position feedback - [OccupancyGrid](/stdlib/messages/occupancy-grid) — 2D grid map - [Pose2D](/stdlib/messages/pose) — Position representation --- ## JointState / JointCommand Path: /stdlib/messages/joint-state Description: Multi-joint feedback and control for robot arms, grippers, and articulated mechanisms # JointState / JointCommand Messages for multi-joint robots: manipulator arms, grippers, legged robots, and any system with named revolute or prismatic joints. `JointState` reports current positions/velocities/efforts; `JointCommand` sends target positions/velocities. ## When to Use Use `JointState` to publish feedback from joint encoders (position, velocity, effort). Use `JointCommand` to send target positions or velocities to a servo controller. Supports up to 16 joints per message. ## ROS2 Equivalent `sensor_msgs/JointState` and `trajectory_msgs/JointTrajectoryPoint` — similar structure. ## JointState — Feedback ### Rust ```rust use horus::prelude::*; let mut state = JointState::default(); state.joint_count = 6; // 6-DOF arm // Set joint names (null-terminated, max 31 chars) state.names[0][..8].copy_from_slice(b"shoulder"); state.names[1][..5].copy_from_slice(b"elbow"); state.names[2][..5].copy_from_slice(b"wrist"); // Set current positions (radians) state.positions[0] = 0.5; // shoulder at 0.5 rad state.positions[1] = -1.2; // elbow at -1.2 rad state.positions[2] = 0.0; // wrist at home let topic: Topic = Topic::new("joint.state")?; topic.send(state); ``` ### Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `names` | `[[u8; 32]; 16]` | -- | Joint names (null-terminated strings) | | `joint_count` | `u8` | -- | Number of active joints (max 16) | | `positions` | `[f64; 16]` | rad / m | Position: radians (revolute) or meters (prismatic) | | `velocities` | `[f64; 16]` | rad/s / m/s | Velocity | | `efforts` | `[f64; 16]` | Nm / N | Torque (revolute) or force (prismatic) | | `timestamp_ns` | `u64` | ns | Timestamp | --- ## JointCommand — Control ### Rust ```rust use horus::prelude::*; let mut cmd = JointCommand::default(); cmd.joint_count = 3; // Target positions cmd.positions[0] = 1.0; // shoulder to 1.0 rad cmd.positions[1] = -0.5; // elbow to -0.5 rad cmd.positions[2] = 0.3; // wrist to 0.3 rad // Velocity limits cmd.velocities[0] = 0.5; // max 0.5 rad/s cmd.velocities[1] = 0.5; cmd.velocities[2] = 1.0; let topic: Topic = Topic::new("joint.command")?; topic.send(cmd); ``` ### Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `joint_names` | `[[u8; 32]; 16]` | -- | Joint names (null-terminated strings) | | `joint_count` | `u8` | -- | Number of active joints (max 16) | | `positions` | `[f64; 16]` | rad / m | Target positions | | `velocities` | `[f64; 16]` | rad/s / m/s | Velocity limits or targets | | `efforts` | `[f64; 16]` | Nm / N | Torque/force limits or targets | | `timestamp_ns` | `u64` | ns | Timestamp | --- ## Common Patterns ### Arm Controller Node ```rust fn tick(&mut self) { // IMPORTANT: always recv() every tick if let Some(cmd) = self.cmd_sub.recv() { for i in 0..cmd.joint_count as usize { // SAFETY: clamp to joint limits let pos = cmd.positions[i].clamp(self.min_limits[i], self.max_limits[i]); self.write_servo(i, pos); } } // Read encoder feedback let mut state = JointState::default(); state.joint_count = self.num_joints; for i in 0..self.num_joints as usize { state.positions[i] = self.read_encoder(i); } self.state_pub.send(state); } fn shutdown(&mut self) -> Result<()> { // SAFETY: move all joints to home position for i in 0..self.num_joints as usize { self.write_servo(i, 0.0); } Ok(()) } ``` ## Related Types - [ServoCommand](/rust/api/control-messages) — Single servo control - [TrajectoryPoint](/rust/api/control-messages) — Timed trajectory waypoint - [Servo Controller](/recipes/servo-controller) — Complete servo bus recipe --- ## AudioFrame Path: /stdlib/messages/audio-frame Description: Audio data from microphones for speech recognition, anomaly detection, and human-robot interaction # AudioFrame Audio data from a microphone or audio source. Fixed-size Pod type for zero-copy shared memory transport. Supports mono, stereo, and multi-channel microphone arrays. ## When to Use Use `AudioFrame` when your robot has microphones and needs to share audio between nodes -- for example, between a microphone driver node, a speech recognition node, and an anomaly detection node. Common use cases: - **Voice commands** -- speech-to-text for human-robot interaction - **Anomaly detection** -- motor fault detection by sound - **Acoustic SLAM** -- using sound for localization - **Teleoperation** -- two-way audio between operator and robot ## ROS2 Equivalent `audio_common_msgs/AudioData` -- similar concept, but HORUS uses a fixed-size Pod buffer for zero-copy SHM instead of variable-length serialized bytes. ## Quick Start ### Rust ```rust use horus::prelude::*; // Publish audio from a microphone let topic: Topic = Topic::new("mic")?; let samples: Vec = capture_audio(); // your mic driver let frame = AudioFrame::mono(16000, &samples); topic.send(frame); // Receive and process let frame = topic.recv().unwrap(); println!("Got {} samples at {}Hz, {:.1}ms", frame.num_samples, frame.sample_rate, frame.duration_ms()); ``` ### Python ```python import horus def process_audio(node): frame = node.get("mic") if frame: samples = frame.samples # list of floats rate = frame.sample_rate # e.g. 16000 duration = frame.duration_ms # e.g. 10.0 # Feed to speech recognition text = whisper.transcribe(samples, sr=rate) node = horus.Node("speech", subs=["mic"], tick=process_audio, rate=100) horus.run(node) ``` ## Constructors ### Rust | Constructor | Description | |-------------|-------------| | `AudioFrame::mono(sample_rate, &samples)` | Single-channel audio | | `AudioFrame::stereo(sample_rate, &samples)` | Interleaved stereo (L R L R...) | | `AudioFrame::multi_channel(sample_rate, channels, &samples)` | Microphone arrays (4, 8, 16 mics) | ### Python ```python # Mono microphone at 16kHz frame = horus.AudioFrame(sample_rate=16000, samples=[0.1, -0.2, 0.3]) # Stereo at 48kHz frame = horus.AudioFrame(sample_rate=48000, channels=2, samples=interleaved) # 4-channel mic array frame = horus.AudioFrame(sample_rate=16000, channels=4, samples=array_data) # With metadata frame = horus.AudioFrame( sample_rate=16000, samples=data, frame_id="mic_left", timestamp_ns=horus.timestamp_ns() ) ``` ## Fields | Field | Type | Unit | Description | |-------|------|------|-------------| | `samples` | `[f32; 4800]` | -- | Audio sample buffer (Rust), `list[float]` (Python -- only valid samples). Range: [-1.0, 1.0] (F32) | | `num_samples` | `u32` | -- | Number of valid samples in buffer | | `sample_rate` | `u32` | Hz | Sample rate (8000, 16000, 44100, 48000) | | `channels` | `u8` | -- | Channel count (1=mono, 2=stereo, N=mic array) | | `encoding` | `u8` | -- | Audio encoding (0=F32, 1=I16) | | `timestamp_ns` | `u64` | ns | Capture timestamp in nanoseconds | | `frame_id` | `[u8; 32]` | -- | Source identifier (e.g. "mic_left") | ## Computed Properties | Property | Type | Description | |----------|------|-------------| | `duration_ms()` | `f64` | Duration of this audio chunk in milliseconds | | `frame_count()` | `u32` | Number of audio frames (samples per channel) | | `valid_samples()` | `&[f32]` | Slice of only the valid samples (Rust) | ## Buffer Size `MAX_AUDIO_SAMPLES = 4800` -- enough for 48kHz at 100ms chunks. For common configurations: | Sample Rate | Chunk Duration | Samples Needed | Fits? | |-------------|---------------|----------------|-------| | 8kHz | 100ms | 800 | Yes | | 16kHz | 20ms | 320 | Yes | | 16kHz | 100ms | 1600 | Yes | | 44.1kHz | 20ms | 882 | Yes | | 48kHz | 100ms | 4800 | Yes (max) | | 48kHz stereo | 50ms | 4800 | Yes (max) | For longer chunks, send multiple frames. ## Multi-Channel Audio For microphone arrays, samples are **interleaved**: channel 0 sample 0, channel 1 sample 0, channel 0 sample 1, channel 1 sample 1, etc. ```rust // 4-channel mic array, 16kHz, 10ms chunk = 640 samples let samples = capture_4ch_audio(); // [ch0_s0, ch1_s0, ch2_s0, ch3_s0, ch0_s1, ...] let frame = AudioFrame::multi_channel(16000, 4, &samples); assert_eq!(frame.frame_count(), 160); // 640 / 4 channels ``` ## AudioEncoding The encoding format for audio samples in the buffer. | Variant | Value | Description | |---------|-------|-------------| | `F32` | 0 | 32-bit float, range [-1.0, 1.0] (normalized) | | `I16` | 1 | 16-bit signed integer, range [-32768, 32767] (PCM) | ```rust use horus::prelude::*; // Float encoding (default, best for processing) let frame = AudioFrame::mono(16000, &float_samples); assert_eq!(frame.encoding, AudioEncoding::F32 as u8); // Integer encoding (common for hardware capture) let mut frame = AudioFrame::default(); frame.encoding = AudioEncoding::I16 as u8; ``` ## Wire Format `AudioFrame` is a fixed-size Pod type (~19.2 KB). It uses the same zero-copy SHM transport as all other Pod messages -- no serialization overhead. ``` [f32 x 4800] samples = 19200 bytes [u32] num_samples = 4 bytes [u32] sample_rate = 4 bytes [u8] channels = 1 byte [u8] encoding = 1 byte [u8 x 2] padding = 2 bytes [u64] timestamp_ns = 8 bytes [u8 x 32] frame_id = 32 bytes Total = 19252 bytes ``` --- ## Message Types Path: /stdlib/messages Description: 55+ standard robotics message types with ROS2 equivalence mapping # Message Types HORUS provides **60+ typed messages** covering every common robotics domain. All types are available in both Rust (`use horus::prelude::*;`) and Python (`from horus import TypeName`). ## Coming from ROS2? | ROS2 Package | ROS2 Message | HORUS Equivalent | |-------------|-------------|-----------------| | `geometry_msgs` | Twist, Pose, Pose2D, TransformStamped, Vector3, Quaternion | Twist, Pose3D, Pose2D, TransformStamped, Vector3, Quaternion | | `sensor_msgs` | Imu, LaserScan, Image, PointCloud2, JointState, BatteryState, CameraInfo | Imu, LaserScan, Image, PointCloud, JointState, BatteryState, CameraInfo | | `nav_msgs` | Odometry, OccupancyGrid, Path | Odometry, OccupancyGrid, NavPath + Waypoint | | `vision_msgs` | Detection2D, Detection3D | Detection, Detection3D | | `audio_common_msgs` | AudioData | AudioFrame | | `std_msgs` | Header | *(embedded — timestamp_ns and frame_id are fields on each message)* | **Key difference from ROS2:** No separate `Header` message. Every HORUS message has `timestamp_ns` and `frame_id` as direct fields. ## Message Categories | Category | Types | Use Case | |----------|-------|----------| | **[Geometry](/rust/api/geometry-messages)** | Pose2D, Pose3D, Twist, Vector3, Point3, Quaternion, TransformStamped, Accel | Position, orientation, motion | | **[Sensors](/rust/api/sensor-messages)** | Imu, LaserScan, Odometry, JointState, BatteryState, Range, Temperature, MagneticField | Sensor data from hardware | | **[Control](/rust/api/control-messages)** | CmdVel, MotorCommand, ServoCommand, JointCommand, PidState | Motor and actuator commands | | **[Navigation](/rust/api/navigation-messages)** | NavGoal, GoalResult, Waypoint, NavPath, OccupancyGrid, CostMap, VelocityObstacle | Path planning and mapping | | **[Perception](/rust/api/perception-messages)** | Detection, Detection3D, TrackedObject, SegmentationMask, LandmarkArray, PlaneDetection | Computer vision and ML output | | **[Vision](/rust/api/vision-messages)** | CompressedImage, CameraInfo, RegionOfInterest, StereoInfo | Camera configuration and compressed data | | **[Force/Haptics](/rust/api/force-messages)** | WrenchStamped, ForceCommand, ContactInfo, ImpedanceParameters, HapticFeedback | Force sensing and control | | **[Diagnostics](/rust/api/diagnostics-messages)** | Heartbeat, DiagnosticStatus, NodeHeartbeat, SafetyStatus, EmergencyStop | System health monitoring | | **[Audio](/stdlib/messages/audio-frame)** | AudioFrame | Microphone data, speech, anomaly detection | | **[Input](/rust/api/input-messages)** | JoystickInput, KeyboardInput | Human input devices | ## Custom Messages Need a type that doesn't exist? Create your own: ```rust use horus::prelude::*; message! { MotorStatus { rpm: f32, current_amps: f32, temperature_c: f32, fault_code: u32, } } // Now use it like any standard message let topic: Topic = Topic::new("motor.status").unwrap(); ``` ======================================== # SECTION: Plugins ======================================== --- ## Creating CLI Plugins Path: /plugins/creating-plugins Description: Build custom CLI plugins that extend the horus command # Creating CLI Plugins A CLI plugin is a standalone binary that adds a subcommand to `horus`. When a user runs `horus mycommand`, HORUS discovers your plugin binary and executes it, passing through all arguments. ## Zero-Config Convention Any Rust package named `horus-*` with a `[[bin]]` target is automatically detected as a plugin. No extra configuration required. **Example:** A package named `horus-sim3d` with `[[bin]] name = "sim3d"` automatically provides the `horus sim3d` command. ## Step-by-Step Guide ### Step 1: Create a New Rust Project ```bash cargo new horus-mycommand cd horus-mycommand ``` ### Step 2: Set Up Cargo.toml ```toml [package] name = "horus-mycommand" version = "0.1.0" edition = "2021" description = "My custom HORUS plugin" [[bin]] name = "mycommand" path = "src/main.rs" [dependencies] clap = { version = "4", features = ["derive"] } ``` The key points: - Package name starts with `horus-` - The `[[bin]]` name defines the subcommand (users will run `horus mycommand`) - Use `clap` or similar for argument parsing ### Step 3: Implement the Plugin ```rust // src/main.rs use clap::Parser; #[derive(Parser)] #[command(name = "mycommand", about = "My custom HORUS command")] struct Cli { /// Target to operate on #[arg(short, long)] target: Option, /// Enable verbose output #[arg(short, long)] verbose: bool, } fn main() { let cli = Cli::parse(); // Check if running as a HORUS plugin if std::env::var("HORUS_PLUGIN").is_ok() { let horus_version = std::env::var("HORUS_VERSION") .unwrap_or_else(|_| "unknown".to_string()); if cli.verbose { eprintln!("Running as HORUS plugin (HORUS v{})", horus_version); } } // Your plugin logic here match cli.target { Some(target) => println!("Operating on: {}", target), None => println!("No target specified. Use --help for usage."), } } ``` ### Step 4: Build and Test Locally ```bash # Build the plugin cargo build --release # Test it standalone ./target/release/mycommand --help # Test it as a HORUS plugin (simulating the environment) HORUS_PLUGIN=1 HORUS_VERSION=0.1.0 ./target/release/mycommand --target foo ``` ### Step 5: Install Locally To test with the actual `horus` CLI, install the binary where HORUS can find it: ```bash # Option A: Copy to global plugin bin directory mkdir -p ~/.horus/bin cp target/release/mycommand ~/.horus/bin/horus-mycommand # Option B: Install via horus from a local path horus install --plugin horus-mycommand --local ``` Now `horus mycommand --help` should work. ## Environment Variables HORUS sets these environment variables when executing plugins: | Variable | Value | Description | |----------|-------|-------------| | `HORUS_PLUGIN` | `1` | Always set when running as a plugin | | `HORUS_VERSION` | e.g., `0.1.7` | Version of the HORUS CLI | Your plugin inherits the user's `stdin`, `stdout`, and `stderr`, so interactive prompts and colored output work normally. ## Plugin Discovery HORUS discovers plugin binaries in this order: 1. **Project `plugins.lock`** — `.horus/plugins.lock` in the current project 2. **Global `plugins.lock`** — `~/.horus/plugins.lock` 3. **Project bin directory** — `.horus/bin/horus-*` 4. **Global bin directory** — `~/.horus/bin/horus-*` 5. **System PATH** — Any `horus-*` binary in `$PATH` The first match wins. This means project-level plugins always override global ones. ## Plugin Detection HORUS discovers plugins by scanning for packages named `horus-*`. The discovery system reads your `Cargo.toml` `[package]` section (name, version, description) and auto-detects the plugin category from the name (e.g., `horus-realsense` → Camera, `horus-rplidar` → LiDAR, `horus-sim3d` → Simulation). ## Security When a plugin is installed through the registry, HORUS records a SHA-256 checksum of the binary. Before each execution, the checksum is verified: - If the binary has been modified, HORUS refuses to run it - Run `horus verify` to check all plugin integrity - Reinstall a plugin with `horus install --plugin ` if verification fails ## Example: A Complete Plugin Here is a minimal but complete plugin that queries HORUS topic statistics: ```rust use clap::Parser; use std::path::PathBuf; #[derive(Parser)] #[command(name = "topic-stats", about = "Show topic statistics summary")] struct Cli { /// Output as JSON #[arg(long)] json: bool, } fn main() -> Result<(), Box> { let cli = Cli::parse(); if !cli.shm_dir.exists() { eprintln!("No HORUS topics found at {}", cli.shm_dir.display()); std::process::exit(1); } let mut topics = Vec::new(); for entry in std::fs::read_dir(&cli.shm_dir)? { let entry = entry?; if entry.file_type()?.is_file() { let name = entry.file_name().to_string_lossy().to_string(); let size = entry.metadata()?.len(); topics.push((name, size)); } } if cli.json { println!("{}", serde_json::to_string_pretty(&topics)?); } else { println!("Found {} topics:", topics.len()); for (name, size) in &topics { println!(" {} ({} bytes)", name, size); } } Ok(()) } ``` **Cargo.toml:** ```toml [package] name = "horus-topic-stats" version = "0.1.0" edition = "2021" description = "Show HORUS topic statistics" [[bin]] name = "topic-stats" path = "src/main.rs" [dependencies] clap = { version = "4", features = ["derive"] } serde_json = "1" ``` After building and installing, users run: ```bash horus topic-stats horus topic-stats --json ``` ## Next Steps - **[Publishing Plugins](/package-management/package-management#publishing-packages)** — Publish your plugin to the HORUS registry - **[Managing Plugins](/plugins/managing-plugins)** — Install, enable/disable, and verify plugins --- ## Managing Plugins Path: /plugins/managing-plugins Description: Install, remove, enable/disable, and verify HORUS plugins # Managing Plugins HORUS provides CLI commands for managing plugins throughout their lifecycle. ## Installing Plugins Plugins are installed through the `horus install --plugin` command. They default to **global** installation since they extend the CLI tool itself. ```bash # Install a plugin globally (default) horus install --plugin horus-sim3d # Install a specific version horus install --plugin horus-sim3d -v 1.2.0 # Install locally to current project only horus install --plugin horus-sim3d --local ``` **What happens during installation:** 1. Downloads the package from the registry 2. Detects plugin from the `horus-*` naming convention 3. Registers the plugin binary in `plugins.lock` 4. Creates symlinks in the bin directory 5. Updates project configuration (for local installs) ### Global vs Local Installation | Aspect | Global | Local | |--------|--------|-------| | **Location** | `~/.horus/cache/` | `.horus/packages/` | | **Scope** | All projects | Current project only | | **Lock file** | `~/.horus/plugins.lock` | `.horus/plugins.lock` | | **Default for plugins** | Yes | No (use `--local`) | | **Override behavior** | — | Overrides global | Local plugins always take priority over global plugins with the same command name. This lets you pin a specific plugin version for a project without affecting other projects. ## Listing Plugins ```bash # List installed plugins horus list # List all plugins including disabled horus list --all ``` ## Searching for Plugins ```bash # Search for plugins by keyword horus search camera # Show all available plugins from registry horus search # Include local development plugins horus search --local # Show detailed info about a specific plugin horus info horus-rplidar ``` ## Removing Plugins ```bash # Remove a plugin horus remove horus-sim3d # Remove from global scope explicitly horus remove horus-sim3d --global ``` ## Enabling and Disabling Plugins You can temporarily disable a plugin without uninstalling it: ```bash # Disable a plugin horus disable sim3d # Disable with a reason horus disable sim3d --reason "Conflicts with sim2d" # Re-enable it horus enable sim3d ``` Disabled plugins remain installed but HORUS will not execute them. ## Verifying Plugin Integrity HORUS records SHA-256 checksums when plugins are installed. Verify that no binaries have been tampered with: ```bash # Verify all plugins horus verify # Verify a specific plugin horus verify sim3d ``` ## The plugins.lock File Plugin registrations are stored in `plugins.lock` (JSON format). You typically don't need to edit this file directly: ```json { "schema_version": "1.0", "scope": "global", "horus_version": "0.1.9", "updated_at": "2025-10-15T10:30:00Z", "plugins": { "sim3d": { "package": "horus-sim3d", "version": "1.0.0", "source": { "type": "registry" }, "binary": "/home/user/.horus/cache/horus-sim3d@1.0.0/bin/sim3d", "checksum": "sha256:abc123...", "installed_at": "2025-10-15T10:30:00Z", "installed_by": "0.1.9", "compatibility": { "horus_min": "0.1.0", "horus_max": "2.0.0" }, "commands": [ { "name": "sim3d", "description": "HORUS 3D robot simulator" } ] } }, "disabled": {}, "inherit_global": true } ``` **File locations:** - Global: `~/.horus/plugins.lock` - Project: `.horus/plugins.lock` ## Using Plugins via `horus install` The `horus install` command provides smart auto-detection. It recognizes plugins automatically: ```bash # Auto-detects as a plugin and installs globally horus install horus-sim3d # Force install as plugin (if auto-detection fails) horus install my-tool --plugin ``` ## Troubleshooting ### Plugin Not Found ``` error: no such command: `mycommand` ``` **Solutions:** ```bash # Check if plugin is installed horus list # Reinstall horus install --plugin horus-mycommand # Check if binary exists in PATH which horus-mycommand ``` ### Checksum Mismatch The binary was modified after installation. Reinstall: ```bash horus install --plugin horus-mycommand ``` ### Plugin Binary Not Found The package is registered but the binary is missing. Reinstall: ```bash horus remove horus-mycommand horus install --plugin horus-mycommand ``` ## Next Steps - **[Creating Plugins](/plugins/creating-plugins)** — Build your own plugin - **[Package Management](/package-management/package-management)** — General package management --- ## Plugins Path: /plugins Description: Extend the HORUS CLI with custom subcommands # Plugins HORUS supports **CLI plugins** — standalone binaries that add new subcommands to the `horus` command. When you run `horus sim3d`, HORUS discovers and executes the `sim3d` plugin binary, passing through all arguments. ## How CLI Plugins Work 1. Any package named `horus-*` with a binary is automatically a plugin 2. Installing it registers the binary with the HORUS plugin system 3. Running `horus ` checks plugins before showing "unknown command" ```bash # Install a plugin horus install --plugin horus-sim3d # Use it as a native command horus sim3d --robot kuka_iiwa # List installed plugins horus list ``` ## Plugin Resolution Order | Priority | Location | Scope | |----------|----------|-------| | 1st | `.horus/plugins.lock` (project) | Local project only | | 2nd | `~/.horus/plugins.lock` (global) | All projects | | 3rd | `.horus/bin/horus-*` (project) | Local project only | | 4th | `~/.horus/bin/horus-*` (global) | All projects | | 5th | `PATH` lookup for `horus-*` binaries | System-wide | Project plugins override global ones, so you can pin a specific version per project. ## Security Plugins are verified via SHA-256 checksums recorded at install time. Before execution, HORUS verifies the binary hasn't been modified: ```bash # Verify all installed plugins horus verify ``` ## Next Steps - **[Creating Plugins](/plugins/creating-plugins)** — Build a plugin that adds commands to `horus` - **[Managing Plugins](/plugins/managing-plugins)** — Install, remove, and configure plugins ======================================== # SECTION: Package Management ======================================== --- ## Using Pre-Built Nodes Path: /package-management/using-prebuilt-nodes Description: The idiomatic way to build HORUS applications with ready-made components # Using Pre-Built Nodes **The HORUS Philosophy:** Don't reinvent the wheel. Use comprehensive, battle-tested nodes from the registry and `horus_library`, then configure them to work together. ## Why Use Pre-Built Nodes? **Advantages of pre-built nodes:** - Production-ready and tested - Configure instead of coding - Focus on application logic, not infrastructure - Nodes use standard HORUS interfaces for interoperability ## Quick Example Instead of writing a PID controller from scratch, just install and configure: ```bash # Install from registry horus install pid-controller ``` ```rust use pid_controller::PIDNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Configure the pre-built node let pid = PIDNode::new(1.0, 0.1, 0.01); // kp, ki, kd scheduler.add(pid).order(5).build()?; scheduler.run()?; Ok(()) } ``` That's it! Production-ready PID control in 3 lines. --- ## Discovering Pre-Built Nodes ### From the Registry **Web Interface:** ```bash # Visit the registry in your browser https://registry.horusrobotics.dev ``` Browse by category: - **Control** - PID controllers, motion planners - **Perception** - Camera, LIDAR, sensor fusion - **Drivers** - Motor controllers, sensor interfaces - **Safety** - Emergency stop, watchdogs - **Utilities** - Loggers, data recorders **CLI Search:** ```bash # Search for specific functionality horus list sensor horus list controller horus list motor ``` ### From Standard Library The `horus_library` crate includes standard message types used across nodes: ```rust use horus::prelude::*; // Motion messages CmdVel, Twist, Pose2D, Odometry // Sensor messages LaserScan, Imu, BatteryState, PointCloud // Input messages KeyboardInput, JoystickInput // And many more... ``` **Note**: Hardware-interfacing nodes (sensor drivers, motor controllers, etc.) are available as **registry packages** or **Python nodes** -- they are not built into `horus_library`. Search the registry for ready-made nodes. --- ## Installation Patterns ### Installing from HORUS Registry ```bash # Latest version horus install motion-planner # Specific version horus install sensor-fusion -v 2.1.0 # Multiple packages horus install pid-controller motion-planner sensor-drivers ``` ### Installing from crates.io ```bash # Rust packages are auto-detected horus install serde horus install tokio -v 1.35.0 ``` ### Installing from PyPI ```bash # Python packages are auto-detected horus install numpy horus install opencv-python ``` ### Using Standard Library The standard library is available automatically with `horus run`: ```bash horus run main.rs # horus_library is included by default ``` Or explicitly in your `Cargo.toml`: ```toml [dependencies] horus = { path = "..." } horus_library = { path = "..." } ``` --- ## The Idiomatic Pattern ### 1. Discover What You Need **Example Goal:** Build a mobile robot with keyboard control **Required Nodes:** - Input: Keyboard control - Control: Velocity command processing - Output: Motor driver ### 2. Search and Install ```bash # Check what's available horus list keyboard horus list motor # Install what you need horus install keyboard-input horus install differential-drive ``` ### 3. Configure and Compose ```rust use keyboard_input::KeyboardNode; use differential_drive::DiffDriveNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Keyboard input node (order 0 - runs first) let keyboard = KeyboardNode::new("keyboard.input")?; scheduler.add(keyboard).order(0).build()?; // Differential drive controller (order 5) let drive = DiffDriveNode::new( "keyboard.input", // Input topic "motor.left", // Left motor output "motor.right", // Right motor output 0.5 // Wheel separation (meters) )?; scheduler.add(drive).order(5).build()?; scheduler.run()?; Ok(()) } ``` **That's it!** A functional robot in ~20 lines, no custom nodes needed. --- ## Common Workflows ### Mobile Robot Base ```bash # Install components horus install keyboard-input horus install differential-drive horus install emergency-stop ``` ```rust use keyboard_input::KeyboardNode; use differential_drive::DiffDriveNode; use emergency_stop::EStopNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Input scheduler.add(KeyboardNode::new("keyboard")?).order(0).build()?; // Safety (runs first!) scheduler.add(EStopNode::new("estop", "cmd_vel")?).order(0).build()?; // Drive control scheduler.add(DiffDriveNode::new("cmd_vel", "motor.left", "motor.right", 0.5)?) .order(1).build()?; scheduler.run()?; Ok(()) } ``` ### Sensor Fusion System ```bash horus install lidar-driver horus install imu-driver horus install kalman-filter ``` ```rust use lidar_driver::LidarNode; use imu_driver::ImuNode; use kalman_filter::EKFNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Sensors (order 2) scheduler.add(LidarNode::new("/dev/ttyUSB0", "scan")?).order(2).build()?; scheduler.add(ImuNode::new("/dev/i2c-1", "imu")?).order(2).build()?; // Fusion (order 3 - runs after sensors) scheduler.add(EKFNode::new("scan", "imu", "pose")?).order(3).build()?; scheduler.run()?; Ok(()) } ``` ### Vision Processing Pipeline ```bash # Install vision packages from registry horus install camera-driver horus install image-processor horus install object-detector ``` ```rust use camera_driver::CameraNode; use image_processor::ImageProcessorNode; use object_detector::ObjectDetectorNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Using registry packages let camera = CameraNode::new("/dev/video0", "camera.raw", 30)?; let processor = ImageProcessorNode::new("camera.raw", "camera.processed")?; let detector = ObjectDetectorNode::new("camera.processed", "objects")?; scheduler.add(camera).order(2).build()?; scheduler.add(processor).order(3).build()?; scheduler.add(detector).order(3).build()?; scheduler.run()?; Ok(()) } ``` --- ## Configuration Best Practices ### Use Builder Patterns Many registry packages support fluent configuration: ```rust // Example: camera-driver package from registry let camera = CameraNode::new("/dev/video0")? .with_resolution(1920, 1080) .with_fps(60) .with_format(ImageFormat::RGB8); scheduler.add(camera).order(2).build()?; ``` ### Parameter-Based Configuration Configure nodes via the parameter system: ```rust use horus::prelude::*; // Set parameters via RuntimeParams let params = RuntimeParams::init()?; params.set("motor.max_speed", 2.0)?; params.set("motor.acceleration", 0.5)?; // Node reads from parameters let motor = MotorNode::from_params()?; scheduler.add(motor).order(1).build()?; ``` **Adjust at runtime via monitor!** ### Reproducible Setup Commit `horus.lock` to git to pin all dependency versions. On another machine, `horus build` will install the exact same versions. --- ## Composing Complex Systems ### Pipeline Pattern Chain nodes together via topics: Sensor"] -->|topic| F["Filter"] F -->|topic| C["Controller"] C -->|topic| A["Actuator"] style S fill:#3b82f6,color:#fff style F fill:#10b981,color:#fff style C fill:#f59e0b,color:#fff style A fill:#ef4444,color:#fff `} /> ```rust // Each node subscribes to previous, publishes to next scheduler.add(sensor).order(2).build()?; // Publishes "raw" scheduler.add(filter).order(3).build()?; // Subscribes "raw", publishes "filtered" scheduler.add(controller).order(4).build()?; // Subscribes "filtered", publishes "cmd" scheduler.add(actuator).order(5).build()?; // Subscribes "cmd" ``` ### Parallel Processing Multiple nodes at same priority run concurrently: ```rust // All run in parallel (order 2) scheduler.add(lidar).order(2).build()?; scheduler.add(camera).order(2).build()?; scheduler.add(imu).order(2).build()?; ``` ### Safety Layering Critical nodes run first: ```rust // Order 0 - Safety checks (runs first) scheduler.add(watchdog).order(0).build()?; scheduler.add(estop).order(0).build()?; // Order 1 - Control scheduler.add(controller).order(1).build()?; // Order 2 - Sensors scheduler.add(lidar).order(2).build()?; // Order 4 - Logging (runs last) scheduler.add(logger).order(4).build()?; ``` --- ## When to Build Custom Nodes **Use pre-built nodes when:** - Functionality exists in the registry or `horus_library` - Node can be configured to your needs - Performance is acceptable **Build custom nodes when:** - No existing node matches your hardware - Unique algorithm or business logic - Extreme performance requirements **Pro tip:** Even then, consider: 1. Starting with a similar pre-built node 2. Forking and modifying it 3. Publishing your improved version back to the registry --- ## Finding the Right Node ### By Use Case **I need to...** - Control a motor `motor-driver`, `differential-drive`, `servo-controller` - Read a sensor `lidar-driver`, `camera-node`, `imu-driver` - Process data `kalman-filter`, `pid-controller`, `image-processor` - Handle safety `emergency-stop`, `safety-monitor`, `watchdog` - Log data `data-logger`, `rosbag-writer`, `csv-logger` ### By Hardware ```bash # Search by device type horus list lidar horus list camera horus list imu ``` ### By Category Browse registry by category: - **control** - Motion control, PID, path following - **perception** - Sensors, computer vision, SLAM - **planning** - Path planning, motion planning - **drivers** - Hardware interfaces - **safety** - Safety systems, fault tolerance - **utils** - Logging, visualization, debugging --- ## Package Quality Indicators When choosing packages, look for: **High Download Count** ``` Downloads: 5,234 (last 30 days) ``` **Recent Updates** ``` Last updated: 2025-09-28 ``` **Good Documentation** ``` Documentation: 98% coverage ``` **Active Maintenance** ``` Issues: 2 open, 45 closed (96% resolution rate) ``` --- ## Complete Example: Autonomous Robot **Goal:** Build an autonomous mobile robot that avoids obstacles **1. Install Components:** ```bash horus install lidar-driver horus install obstacle-detector horus install path-planner horus install differential-drive horus install emergency-stop ``` **2. Compose System:** ```rust use lidar_driver::LidarNode; use obstacle_detector::ObstacleDetectorNode; use path_planner::LocalPlannerNode; use differential_drive::DiffDriveNode; use emergency_stop::EStopNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Safety (order 0 - runs first) scheduler.add(EStopNode::new("estop", "cmd_vel")?).order(0).build()?; // Sensors (order 1) scheduler.add(LidarNode::new("/dev/ttyUSB0", "scan")?).order(1).build()?; // Perception (order 2) scheduler.add(ObstacleDetectorNode::new("scan", "obstacles")?).order(2).build()?; // Planning (order 3) scheduler.add(LocalPlannerNode::new("obstacles", "cmd_vel")?).order(3).build()?; // Control (order 4) scheduler.add(DiffDriveNode::new("cmd_vel", "motor.left", "motor.right", 0.5)?).order(4).build()?; scheduler.run()?; Ok(()) } ``` **That's a full autonomous robot in ~40 lines of configuration!** --- ## Next Steps - **[Package Management](/package-management/package-management)** - Discover and manage packages - **[node! Macro](/concepts/node-macro)** - When you need custom functionality - **[Examples](/rust/examples/basic-examples)** - See complete working systems --- ## Package Management Path: /package-management/package-management Description: Install, publish, and manage reusable HORUS components # Package Management > **Note**: Publishing packages requires the registry backend to be deployed. Installing public packages works immediately. HORUS provides a comprehensive package management system for sharing and discovering robotics components. Create reusable nodes, message types, and algorithms that the community can use. ## Overview The package system allows you to: - **Install packages** from multiple sources (HORUS registry, crates.io, PyPI) - **Publish your work** for others to use - **Manage dependencies** automatically - **Version control** with semantic versioning - **Search and discover** community packages ## Package Sources HORUS supports installing packages from multiple sources: | Source | Description | Example | |--------|-------------|---------| | **HORUS Registry** | Curated robotics packages | `horus install pid-controller` | | **crates.io** | Rust ecosystem packages | `horus install serde` | | **PyPI** | Python ecosystem packages | `horus install numpy` | | **Git** | Git repositories (via `horus.toml`) | See [Configuration](/package-management/configuration) | | **Local Path** | Local filesystem (via `horus.toml`) | See [Configuration](/package-management/configuration) | ## Quick Start ### Installing a Package ```bash # Install from HORUS registry horus install pid-controller # Install from crates.io (auto-detected) horus install serde horus install tokio # Install from PyPI (auto-detected) horus install numpy horus install opencv-python # Install specific version horus install serde -v 1.0.200 # Install globally (share across all projects) horus install sensor-drivers -g ``` ### Automatic Source Detection HORUS automatically detects the package source: 1. First checks HORUS registry 2. Then checks both PyPI and crates.io 3. If found in multiple sources, prompts you to choose: ``` Package 'package_name' found in BOTH PyPI and crates.io Which package source do you want to use? [1] [PYTHON] PyPI (Python package) [2] [RUST] crates.io (Rust binary) [3] [FAIL] Cancel installation Choice [1-3]: ``` ### System Package Detection If a package is already installed system-wide, HORUS offers to reuse it: ``` Package 'ripgrep' v14.0.0 already installed system-wide [1] Use system package (no download) [2] Install fresh copy to HORUS [3] Cancel Choice [1-3]: ``` **What happens during installation:** 1. Detects package source (HORUS registry, crates.io, or PyPI) 2. Downloads package from the appropriate source 3. Resolves dependencies automatically 4. Caches locally in `~/.horus/cache/` or `.horus/packages/` 5. Makes package available for use ### Using an Installed Package ```rust // In your main.rs or any file use pid_controller::PIDNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Use the installed package let pid = PIDNode::new(1.0, 0.1, 0.01); scheduler.add(pid).order(5).build()?; scheduler.run()?; Ok(()) } ``` ### Publishing Your Package ```bash # 1. Authenticate first (one-time) horus auth login # 2. Navigate to your project cd my-awesome-controller # 3. Publish horus publish ``` ## Package Locations ### Local Packages **Project-local** (default): ``` my_project/ ── .horus/ ── packages/ ── pid-controller@1.0.0/ # HORUS registry ── serde@1.0.200/ # crates.io ── pypi_numpy@1.24.0/ # PyPI (prefixed with pypi_) ── src/ ── main.rs ``` **Why use local:** - Different projects can use different versions - Clean separation per project - Easy to delete with project ### Global Packages **System-wide** (installed with `-g` flag): ``` ~/.horus/ ── cache/ ── pid-controller@1.0.0/ # HORUS registry ── serde@1.0.200/ # crates.io ── pypi_numpy@1.24.0/ # PyPI packages ── git_abc123/ # Git dependencies ``` **Naming conventions by source:** | Source | Directory Format | Example | |--------|------------------|---------| | HORUS Registry | `\@\/` | `pid-controller@1.0.0/` | | crates.io | `\@\/` | `serde@1.0.200/` | | PyPI | `pypi_\@\/` | `pypi_numpy@1.24.0/` | | Git | `git_\/` | `git_abc123def/` | **Why use global:** - Share common packages across all projects - Save disk space (one copy for everything) - Faster install after first download ### Priority Order & Smart Dependency Resolution When resolving packages, HORUS checks in this order: **1. Project-local `.horus/packages/` (highest priority)** - Checked first, ALWAYS wins - Can be symlink to global OR real directory - Enables local override of broken global packages **2. Global cache `~/.horus/cache/`** - Only checked if not found locally - Shared across all projects - Version-specific directories (e.g., `serde@1.0.228/`) **3. System install `/usr/local/lib/horus/` (if available)** - Last resort fallback **Smart Installation Behavior:** When you run `horus install`, HORUS automatically chooses the best strategy: ```bash # Default behavior (no flags) horus install serde # If package exists in global cache: # Install to global cache # Create symlink: .horus/packages/serde -> ~/.horus/cache/serde@1.0.228/ # Disk efficient! # If package NOT in global cache: # Install directly to .horus/packages/serde@1.0.228/ # No symlink, real directory # Isolated from global! ``` **Override Broken Global Cache:** Local packages always win, so you can override corrupted global packages: ```bash # Scenario: Global cache has broken serde@1.0.228 ~/.horus/cache/serde@1.0.228/ # Corrupted # Fix: Install working version locally rm .horus/packages/serde # Remove symlink to broken global horus install serde -v 1.0.150 # Install working version locally # Result: .horus/packages/serde@1.0.150/ # Real directory, not symlink # horus run will use this, ignoring broken global! ``` **Benefits:** - **Local override** - Bypass broken global packages - **Version isolation** - Different projects can use different versions - **Disk efficient** - Shares global cache when possible - **Zero config** - Works automatically Commit `horus.lock` to git so teammates get identical dependency versions with `horus build`. ## Package Commands ### `horus install` Install packages from multiple sources (HORUS registry, crates.io, PyPI). **Usage:** ```bash horus install [OPTIONS] ``` **Options:** - `-v, --ver ` - Install specific version (default: latest) - `-g, --global` - Install to global cache - `-t, --target ` - Target workspace/project name **Examples:** ```bash # From HORUS registry horus install pid-controller horus install motion-planner -v 2.0.1 # From crates.io (auto-detected) horus install serde horus install tokio -v 1.35.0 horus install clap # From PyPI (auto-detected) horus install numpy horus install opencv-python -v 4.8.0 horus install torch # Global installation horus install serde -g # Install to specific workspace horus install pid-controller -t my-project ``` #### Installing from crates.io When installing Rust packages from crates.io, HORUS uses `cargo install` under the hood: ```bash horus install ripgrep ``` **Output:** ``` Installing ripgrep from crates.io... Compiling ripgrep... Installing with cargo... Package installed: ripgrep@14.0.0 Location: ~/.horus/cache/ripgrep@14.0.0/ ``` **Requirements:** - Rust toolchain must be installed (`rustup`) - `cargo` must be available in PATH #### Installing from PyPI When installing Python packages from PyPI, HORUS uses `pip install --target` to isolate packages: ```bash horus install numpy ``` **Output:** ``` Installing numpy from PyPI... Downloading numpy-1.24.0... Installing to .horus/packages/pypi_numpy@1.24.0/ Package installed: numpy@1.24.0 Location: .horus/packages/pypi_numpy@1.24.0/ ``` **Requirements:** - Python 3.x must be installed - `pip` must be available in PATH #### Using Python Packages After installing a PyPI package, use it in your Python nodes: ```python # In your Python node import sys sys.path.insert(0, '.horus/packages/pypi_numpy@1.24.0') import numpy as np # Or HORUS automatically adds package paths when using horus run ``` When using `horus run`, Python package paths are automatically configured. **HORUS Registry Output:** ``` Installing pid-controller@1.2.0... Downloaded (245 KB) Extracted to .horus/packages/pid-controller@1.2.0/ Installed dependencies: control-utils@1.0.0 Build successful Package installed: pid-controller@1.2.0 Location: .horus/packages/pid-controller@1.2.0/ Usage: use pid_controller::PIDNode; ``` ### `horus remove` Uninstall a package. **Usage:** ```bash horus remove ``` **Options:** - `-g, --global` - Remove from global cache - `-t, --target ` - Target workspace/project name **Examples:** ```bash # Remove local package horus remove motion-planner # Remove from global cache horus remove common-utils -g # Remove from specific workspace horus remove pid-controller -t my-project ``` **Output:** ``` Removing pid-controller@1.2.0... Removed from .horus/packages/ Freed 892 KB Package removed: pid-controller@1.2.0 ``` ### `horus list` List installed packages or search the registry. **Usage:** ```bash horus list [QUERY] [OPTIONS] ``` **Options:** - `-g, --global` - List global cache packages - `-a, --all` - List all (local + global) **List Local Packages:** ```bash horus list ``` **Output:** ``` Local packages: pid-controller 1.2.0 motion-planner 2.0.1 sensor-drivers 1.5.0 ``` **List Global Cache:** ```bash horus list -g ``` **Search Registry:** ```bash # Search by keyword horus list sensor ``` **Output:** ``` Found 3 package(s): sensor-fusion 2.1.0 - Kalman filter fusion sensor-drivers 1.5.0 - LIDAR/IMU/camera drivers sensor-calibration 1.0.0 - Calibration tools ``` ### `horus update` Update installed packages to their latest versions. **Usage:** ```bash horus update [PACKAGE] [OPTIONS] ``` **Options:** - `-g, --global` - Update global cache packages - `--dry-run` - Show what would be updated without making changes **Examples:** ```bash # Update all local packages horus update # Update a specific package horus update pid-controller # Update global packages horus update -g # Preview updates without applying horus update --dry-run ``` ### `horus unpublish` Remove a package version from the registry (irreversible!). **Usage:** ```bash horus unpublish [OPTIONS] ``` **Options:** - `-y, --yes` - Skip confirmation prompt **Examples:** ```bash # Unpublish a specific version horus unpublish my-package 1.0.0 # Skip confirmation prompt horus unpublish my-package 1.0.0 -y ``` **Output:** ``` Unpublishing my-package v1.0.0... Warning: This action is IRREVERSIBLE and will: • Delete my-package v1.0.0 from the registry • Make this version unavailable for download • Cannot be undone Type the package name 'my-package' to confirm: my-package Successfully unpublished my-package v1.0.0 The package is no longer available on the registry ``` > **Note**: Detailed package information can be viewed on the registry web interface at https://registry.horusrobotics.dev ## Authentication (for Publishing) > **Note**: Registry publishing and private resources require the registry backend to be deployed. GitHub authentication is fully functional. Authentication is required for publishing packages, sharing environments, and accessing private registry resources. HORUS uses GitHub OAuth for interactive login and API keys for automated systems. ###Authentication Overview **Authentication methods:** - **GitHub OAuth** - Interactive login via browser (recommended for development) - **API Keys** - Long-lived tokens for CI/CD and automation - **Environment Variables** - For containerized deployments **What requires authentication:** - Publishing packages (`horus publish`) - Accessing private packages - Managing your published packages **What doesn't require authentication:** - Installing public packages (`horus install`) - Searching registry (`horus list`) - Using installed packages ### Quick Authentication Setup **Interactive Login:** ```bash # Login with GitHub horus auth login ``` **What happens:** 1. Opens browser to GitHub OAuth page 2. You authorize HORUS Registry 3. Token saved to `~/.horus/auth.json` 4. Ready to publish! **Check Authentication:** ```bash # Verify you're logged in horus auth whoami ``` **Logout:** ```bash # Remove credentials horus auth logout ``` ### GitHub OAuth Login **First-Time Setup:** ```bash # Run login command horus auth login ``` **Output:** ``` Opening GitHub OAuth page in browser... If browser doesn't open automatically, visit: https://github.com/login/oauth/authorize?client_id=... Waiting for authorization... ``` **In browser:** 1. See "Authorize HORUS Registry" page 2. Review permissions requested: - Read user profile - Read email address 3. Click "Authorize horus-registry" 4. Redirected to success page **Back in terminal:** ``` Authorization successful! Token saved to ~/.horus/auth.json Authenticated as: your-username Email: you@example.com You can now publish packages with: horus publish ``` **What Gets Stored:** Credentials file: `~/.horus/auth.json` ```json { "api_key": "horus_key_abc123def456...", "registry_url": "https://registry.horusrobotics.dev", "cloud_url": "https://cloud.horus.dev", "github_username": "your-username" } ``` **Security:** - File permissions: `0600` (read/write owner only) - Revocable via GitHub settings or registry web interface **Token Permissions:** Required scopes: - `read:user` - Read your GitHub profile - `user:email` - Read your email address Not requested: - No write access to repositories - No access to private repositories - No access to organizations **Revoking Access:** Via GitHub: 1. Go to https://github.com/settings/applications 2. Find "HORUS Registry" under "Authorized OAuth Apps" 3. Click "Revoke" Via CLI: ```bash horus auth logout ``` ### API Keys (for CI/CD) API keys are long-lived tokens for automated systems like CI/CD pipelines. **Generating API Keys:** ```bash # Interactive generation horus auth generate-key ``` **Interactive prompts:** ``` Generating API key... Key name (for identification): CI/CD Pipeline Environment (optional): production Description (optional): GitHub Actions deployment Generated API key: horus_key_abc123def456ghi789jkl012mno345pqr678stu901 WARNING: Copy this key now. It won't be shown again. Save to environment variable: export HORUS_API_KEY=horus_key_abc123def456ghi789jkl012mno345pqr678stu901 Or use in CI: # GitHub Actions - name: Publish env: HORUS_API_KEY: ${{ secrets.HORUS_API_KEY }} run: horus publish ``` **With flags:** ```bash horus auth generate-key \ --name "GitHub Actions" \ --environment "production" ``` **Using API Keys:** Environment variable (recommended): ```bash # Set for session export HORUS_API_KEY=horus_key_abc123... # Use horus commands horus publish ``` Credentials file: ```bash # Save to file manually echo '{"api_key":"horus_key_abc123..."}' > ~/.horus/auth.json ``` **Managing API Keys:** ```bash # List all API keys horus auth keys list # Revoke a specific key horus auth keys revoke horus_key_abc123... ``` ### CI/CD Integration **GitHub Actions:** Workflow file (`.github/workflows/publish.yml`): ```yaml name: Publish to HORUS Registry on: push: tags: - 'v*' jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable - name: Install HORUS run: | git clone https://github.com/softmata/horus.git /tmp/horus cd /tmp/horus && ./install.sh - name: Publish Package env: HORUS_API_KEY: ${{ secrets.HORUS_API_KEY }} run: | horus publish ``` **Setup:** 1. Generate API key: `horus auth generate-key --name "GitHub Actions"` 2. Copy key: `horus_key_abc123...` 3. Add to GitHub secrets: - Go to repository Settings → Secrets → Actions - New repository secret: `HORUS_API_KEY` - Paste key value 4. Push tag: `git tag v1.0.0 && git push origin v1.0.0` **GitLab CI:** `.gitlab-ci.yml`: ```yaml publish: stage: deploy image: rust:latest before_script: - git clone https://github.com/softmata/horus.git /tmp/horus - cd /tmp/horus && ./install.sh script: - horus publish only: - tags variables: HORUS_API_KEY: $CI_HORUS_API_KEY ``` **Setup:** 1. Generate key: `horus auth generate-key --name "GitLab CI"` 2. Add to GitLab: - Settings → CI/CD → Variables - Key: `CI_HORUS_API_KEY` - Value: `horus_key_abc123...` - Type: Masked **Docker:** Dockerfile: ```dockerfile FROM rust:1.70 # Install HORUS RUN git clone https://github.com/softmata/horus.git /tmp/horus \ && cd /tmp/horus && ./install.sh \ && rm -rf /tmp/horus # Copy project COPY . /app WORKDIR /app # Build and publish ARG HORUS_API_KEY ENV HORUS_API_KEY=${HORUS_API_KEY} RUN horus publish ``` Build: ```bash docker build \ --build-arg HORUS_API_KEY=$HORUS_API_KEY \ -t my-horus-app . ``` **Jenkins:** Jenkinsfile: ```groovy pipeline { agent any environment { HORUS_API_KEY = credentials('horus-api-key') } stages { stage('Setup') { steps { sh 'git clone https://github.com/softmata/horus.git /tmp/horus && cd /tmp/horus && ./install.sh' } } stage('Build') { steps { sh 'horus run --build-only --release' } } stage('Publish') { when { buildingTag() } steps { sh 'horus publish' } } } } ``` **Setup:** 1. Generate key: `horus auth generate-key --name "Jenkins"` 2. Jenkins monitor → Manage Jenkins → Credentials 3. Add secret text credential: `horus-api-key` ### Environment Variables **Available Variables:** **`HORUS_API_KEY`** - API key for authentication - Overrides credentials file - Required for CI/CD **`HORUS_REGISTRY_URL`** - Override registry URL for self-hosted registries - Only needed if running your own registry instance **Example usage:** ```bash export HORUS_API_KEY=horus_key_abc123... horus publish ``` ### Security Best Practices **Token Security - Do's:** - Use API keys for CI/CD (not OAuth tokens) - Rotate keys every 90 days - Use different keys for different environments - Store keys in CI/CD secrets management - Revoke unused keys immediately - Use environment variables (not command-line flags) **Token Security - Don'ts:** - Never commit credentials to git - Never share API keys between team members - Never log API keys - Never use same key for prod and dev - Never hardcode keys in source code **Credentials File Security:** File permissions: ```bash # Verify permissions ls -la ~/.horus/auth.json # Should show: -rw------- (600) # Fix if needed chmod 600 ~/.horus/auth.json ``` Backup: ```bash # Backup credentials (encrypted) gpg -c ~/.horus/auth.json # Creates ~/.horus/auth.json.gpg # Restore gpg -d ~/.horus/auth.json.gpg > ~/.horus/auth.json chmod 600 ~/.horus/auth.json ``` **Key Rotation:** Regular rotation schedule: ```bash # 1. Generate new key horus auth generate-key --name "Production 2025-Q4" # 2. Update CI/CD secrets with the new key # (Do this manually in your CI/CD platform) # 3. Test new key export HORUS_API_KEY=horus_key_new_key... horus auth whoami # 4. Revoke old key horus auth keys revoke horus_key_old_key_id... ``` ### Authentication Troubleshooting **Authentication Failed:** Error: ``` Error: Authentication required Run: horus auth login ``` Solutions: ```bash # Check current status horus auth whoami # Re-authenticate horus auth login # Verify credentials file exists ls -la ~/.horus/auth.json ``` **Token Expired:** Error: ``` Error: Token expired Please re-authenticate ``` Solutions: ```bash # Re-login (auto-refreshes token) horus auth login # Or use API key instead horus auth generate-key export HORUS_API_KEY=horus_key_abc123... ``` **Invalid API Key:** Error: ``` Error: Invalid API key Status: 401 Unauthorized ``` Causes: - Key was revoked - Key expired - Typo in key value - Wrong registry URL Solutions: ```bash # Verify key format echo $HORUS_API_KEY # Should start with: horus_key_ # Check key status via registry web interface # Visit https://registry.horusrobotics.dev # Generate new key horus auth generate-key ``` **Permission Denied:** Error: ``` Error: Permission denied You don't have permission to publish to this package ``` Causes: - Package owned by another user - Not logged in - Insufficient permissions Solutions: ```bash # Verify authentication horus auth whoami # Check package ownership via registry web interface # Visit https://registry.horusrobotics.dev # For package ownership transfer, contact registry support ``` **GitHub OAuth Failed:** Error: ``` Error: OAuth authorization failed Could not complete GitHub authentication ``` Solutions: ```bash # Re-login horus auth login # Check browser popup blockers # Verify GitHub account curl https://api.github.com/user \ -H "Authorization: Bearer " ``` ### Advanced Authentication Topics **Self-Hosted Registry:** Configure custom registry: ```bash # Set registry URL export HORUS_REGISTRY_URL=https://registry.company.internal # Authenticate horus auth login # Use as normal horus publish ``` **Multiple Accounts:** Switch between accounts: ```bash # Save current credentials mv ~/.horus/auth.json ~/.horus/auth.json.account1 # Login with second account horus auth login # Switch back mv ~/.horus/auth.json ~/.horus/auth.json.account2 mv ~/.horus/auth.json.account1 ~/.horus/auth.json ``` ## Publishing Packages ### Prerequisites Before publishing: 1. **Authenticate** with the registry (see [Authentication](#authentication-for-publishing) section above): ```bash horus auth login ``` 2. **Complete `horus.toml`** metadata: ```toml [package] name = "my-awesome-package" version = "1.0.0" description = "Brief description of your package" license = "MIT" ``` During publishing, HORUS will interactively prompt you for optional metadata like categories, package type, documentation URL, and source repository. 3. **Test your package** locally: ```bash horus run --release ``` ### `horus publish` **Usage:** ```bash horus publish [OPTIONS] ``` **Options:** - `--dry-run` - Validate package without actually uploading ### Publishing Workflow ```bash # 1. Navigate to package directory cd my-awesome-package # 2. Verify everything builds horus run --build-only --release # 3. Publish (or dry-run first) horus publish --dry-run horus publish ``` **Output:** ``` Detected horus.toml manifest Publishing my-awesome-package v1.0.0... Uploaded to registry Published: my-awesome-package@1.0.0 View at: https://registry.horusrobotics.dev/packages/my-awesome-package ``` After uploading, HORUS interactively prompts for optional metadata (categories, package type, documentation, source repository). ### Adding Documentation and Source Links After publishing, you'll be prompted to add optional metadata to help users discover and use your package: #### Documentation Options **External Documentation URL:** Link to your hosted documentation website (e.g., GitHub Pages, ReadTheDocs, custom site): ``` Documentation Add documentation? (y/n): y Documentation options: 1. External URL - Link to online documentation 2. Local /docs - Bundle markdown files in a /docs folder Choose option (1/2/skip): 1 Enter documentation URL: https://my-package-docs.example.com Documentation URL: https://my-package-docs.example.com ``` **Local Documentation (Bundled Markdown):** Include markdown files directly in your package for built-in documentation viewing: ``` Documentation Found local /docs folder with markdown files Add documentation? (y/n): y Documentation options: 1. External URL - Link to online documentation 2. Local /docs - Bundle markdown files in a /docs folder [i] Your /docs folder should contain .md files organized as: /docs/README.md (main documentation) /docs/getting-started.md (guides) /docs/api.md (API reference) Choose option (1/2/skip): 2 Will bundle local /docs folder with package ``` **Local Docs Structure:** ``` my-package/ ── docs/ ── README.md # Main documentation page ── getting-started.md # Installation and setup guide ── api.md # API reference ── examples.md # Usage examples ── src/ ── lib.rs ── horus.toml ``` **Benefits of Local Docs:** - Users can view docs directly from the registry - Works offline - Version-specific documentation - Automatic rendering with syntax highlighting - No external hosting required #### Source Repository Link to your GitHub, GitLab, or other repository: ``` Source Repository Auto-detected: https://github.com/username/my-package Add source repository? (y/n): y Use detected URL? (y/n): y Source repository: https://github.com/username/my-package ``` **Manual Entry:** If auto-detection doesn't work or you want to use a different URL: ``` Source Repository Add source repository? (y/n): y [i] Enter the URL where your code is hosted: • GitHub: https://github.com/username/repo • GitLab: https://gitlab.com/username/repo • Other: Any public repository URL Enter source repository URL: https://gitlab.com/robotics/my-package Source repository: https://gitlab.com/robotics/my-package ``` #### Complete Publishing Example ```bash $ cd my-sensor-package $ horus publish Publishing my-sensor-package v1.0.0... Uploaded to registry Published: my-sensor-package@1.0.0 View at: https://registry.horusrobotics.dev/packages/my-sensor-package Package Metadata (optional) Help users discover and use your package by adding: Documentation Found local /docs folder with markdown files Add documentation? (y/n): y Documentation options: 1. External URL - Link to online documentation 2. Local /docs - Bundle markdown files in a /docs folder [i] Your /docs folder should contain .md files organized as: /docs/README.md (main documentation) /docs/getting-started.md (guides) /docs/api.md (API reference) Choose option (1/2/skip): 2 Will bundle local /docs folder with package Source Repository Auto-detected: https://github.com/robotics-lab/my-sensor-package Add source repository? (y/n): y Use detected URL? (y/n): y Source repository: https://github.com/robotics-lab/my-sensor-package Updating package metadata... Package metadata updated! ``` #### How Users See Your Links On the registry, your package will display: my-sensor-package v1.0.0"] DESC["High-performance sensor fusion"] BTNS["[View Details] [Docs] [Source]"] LINKS["Markdown Viewer | GitHub Repo"] end `} caption="Registry package display with documentation and source links" /> - **Docs Button**: Only appears if you added documentation - External URL: Opens in new tab - Local docs: Opens built-in markdown viewer - **Source Button**: Only appears if you added source URL - Opens repository in new tab ### Version Management **Semantic Versioning:** - `1.0.0` - Major.Minor.Patch - `1.0.0` `1.0.1` - Patch: Bug fixes only - `1.0.0` `1.1.0` - Minor: New features (backward compatible) - `1.0.0` `2.0.0` - Major: Breaking changes **Publishing new version:** ```bash # 1. Update version in horus.toml # version = "1.1.0" # 2. Publish horus publish ``` **Version constraints when installing:** ```bash horus add pid-controller@1.2.0 # Exact version horus add motion-planner@^2.0 # Compatible (2.x.x, not 3.0.0) horus add sensor-drivers@~1.5.0 # Patch updates (1.5.x) ``` ## Dependency Management ### Automatic Resolution HORUS automatically resolves and installs dependencies: ```bash horus install robot-controller ``` **Output:** ``` Resolving dependencies... robot-controller@1.0.0 ── motion-planner@2.0.1 ── pathfinding-utils@1.2.0 ── pid-controller@1.2.0 ── control-utils@1.0.0 Installing 5 packages... All dependencies installed ``` ### Specifying Dependencies Dependencies live in your native build files. For Rust projects, add to `Cargo.toml`: ```toml [dependencies] horus = { path = "..." } serde = { version = "1", features = ["derive"] } ``` For HORUS registry packages, use `horus add`: ```bash horus add pid-controller horus add motion-planner ``` See [Configuration Reference](/package-management/configuration) for details on project configuration. ## Package Structure ### Minimal Package ``` my-package/ ── horus.toml # Package metadata ── src/ ── lib.rs # Library entry point ── nodes/ ── my_node.rs # Your node implementation ── examples/ ── demo.rs # Usage example ── README.md # Documentation ``` ### Library Package (`lib.rs`) ```rust // src/lib.rs pub mod nodes; pub mod messages; pub mod utils; // Re-export commonly used items pub use nodes::MyControllerNode; pub use messages::MyMessage; ``` ### Node Implementation ```rust // src/nodes/my_node.rs use horus::prelude::*; pub struct MyControllerNode { pub input: Topic, pub output: Topic, gain: f64, } impl MyControllerNode { pub fn new(gain: f64) -> Self { Self { input: Topic::new("input").expect("Failed to create input topic"), output: Topic::new("output").expect("Failed to create output topic"), gain, } } } impl Node for MyControllerNode { fn name(&self) -> &str { "MyController" } fn tick(&mut self) { if let Some(value) = self.input.recv() { let result = value * self.gain; self.output.send(result); } } } ``` ### Example Usage ```rust // examples/demo.rs use my_package::MyControllerNode; use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new(); let controller = MyControllerNode::new(2.5); scheduler.add(controller).order(5).build()?; scheduler.run()?; Ok(()) } ``` **Test the example:** ```bash horus run examples/demo.rs --release ``` ## Best Practices ### Package Design **Single Responsibility:** ```bash # Good: Focused packages pid-controller # Just PID control motion-planner # Just path planning sensor-fusion # Just sensor fusion # Bad: Kitchen sink package robotics-everything # Too broad, hard to maintain ``` **Clear Interfaces:** ```rust // Good: Simple, clear API pub struct PIDController { pub fn new(kp: f64, ki: f64, kd: f64) -> Self { ... } pub fn update(&mut self, error: f64) -> f64 { ... } } // Bad: Complex, unclear API pub struct Controller { pub fn do_stuff(&mut self, x: f64, y: Option, z: &str) -> Result, Box> { ... } } ``` ### Documentation **Include comprehensive README:** ```markdown # PID Controller Production-ready PID controller for HORUS robotics framework. ## Features - Anti-windup protection - Derivative filtering - Output clamping ## Installation ```bash horus install pid-controller ``` ## Usage ```rust use pid_controller::PIDController; let mut pid = PIDController::new(1.0, 0.1, 0.01); let output = pid.update(error); ``` ## Examples See `examples/` directory for complete examples. ## License MIT ``` ### Testing **Always test before publishing:** ```bash # Run tests horus test # Run examples horus run examples/demo.rs --release # Build in release mode horus run --build-only --release ``` ### Versioning Strategy **Semantic Versioning:** - `0.x.x` - Development (expect breaking changes) - `1.0.0` - First stable release - `1.x.x` - Stable with backward compatibility - `2.0.0` - Major rewrite or breaking changes **Changelog:** ```markdown # Changelog ## [1.2.0] - 2025-10-09 ### Added - Anti-windup protection - Configurable output limits ### Fixed - Derivative kick on setpoint change ## [1.1.0] - 2025-09-15 ### Added - Derivative filtering ## [1.0.0] - 2025-08-01 - Initial stable release ``` ## Common Workflows ### Creating a Package Library ```bash # 1. Create new project as library horus new my-sensor-lib --rust # 2. Update horus.toml # [package] # name = "my-sensor-lib" # version = "0.1.6" # # [lib] # name = "my_sensor_lib" # path = "src/lib.rs" # 3. Implement in src/lib.rs pub mod drivers; pub mod calibration; # 4. Add examples mkdir examples # Create examples/demo.rs # 5. Test horus run examples/demo.rs # 6. Publish horus auth login horus publish ``` ### Using Multiple Packages ```bash # Install packages horus install pid-controller horus install motion-planner horus install sensor-fusion # Use in your project ``` ```rust use pid_controller::PIDController; use motion_planner::AStarPlanner; use sensor_fusion::KalmanFilter; use horus::prelude::*; fn main() { let mut scheduler = Scheduler::new(); // Combine multiple packages let pid = PIDController::new(1.0, 0.1, 0.01); let planner = AStarPlanner::new(); let filter = KalmanFilter::new(); // Add nodes... } ``` ### Updating Dependencies ```bash # Update all packages to latest versions horus update # Update a specific package horus update pid-controller # Or install a specific version horus install pid-controller -v 1.3.0 # Check available versions on registry horus list pid-controller ``` ## Troubleshooting ### Package Not Found **Error:** ``` Error: Package 'nonexistent-package' not found in registry ``` **Solutions:** ```bash # Check spelling horus list nonexistent # Search registry for correct package name horus list correct-package ``` ### Version Conflict **Error:** ``` Error: Version conflict robot-controller requires motion-planner ^2.0 sensor-fusion requires motion-planner ^1.5 ``` **Solutions:** ```bash # Option 1: Update conflicting package horus install sensor-fusion -v 2.0.0 # Install compatible version # Option 2: Pin version in your native build file (Cargo.toml or pyproject.toml) ``` ### Build Failures **Error:** ``` Error: Failed to build package 'my-package' ``` **Solutions:** ```bash # Clean and rebuild horus remove my-package horus install my-package # Check dependencies via registry web interface # Visit https://registry.horusrobotics.dev # Install dependencies manually if needed horus install dependency-name ``` ### Authentication Required **Error:** ``` Error: Authentication required to publish packages Run: horus auth login ``` **Solution:** ```bash horus auth login # Opens browser for GitHub OAuth ``` ### Registry Unavailable **Error:** ``` Error: Failed to connect to registry ``` **Solutions:** ```bash # Check internet connection ping registry.horusrobotics.dev # Try again later (registry might be down) # Use cached packages if available ls ~/.horus/cache/ ``` ## Registry API ### Direct API Access You can interact with the registry programmatically: **Search packages:** ```bash curl https://registry.horusrobotics.dev/api/packages?q=sensor ``` **Get package info:** ```bash curl https://registry.horusrobotics.dev/api/packages/pid-controller ``` **Download package:** ```bash curl -o pkg.tar.gz https://registry.horusrobotics.dev/api/packages/pid-controller/1.2.0/download ``` ## Next Steps - **[CLI Reference](/development/cli-reference)** - Complete command documentation --- ## Lockfile & Reproducibility Path: /package-management/lockfile Description: How horus.lock pins dependencies for reproducible builds across machines and platforms # Lockfile & Reproducibility `horus.lock` is the single file that ensures every machine builds with identical dependencies. It pins exact versions for packages, toolchains, and system libraries. ``` horus.toml → what you WANT (version ranges, declarative) horus.lock → what you GOT (exact pins, reproducible) ``` This is the same pattern as `Cargo.lock`, `package-lock.json`, or `uv.lock`. ## How It Works ```bash # First build: resolves dependencies, creates horus.lock horus build # Subsequent builds: uses pinned versions from horus.lock horus build # Teammate clones repo, gets identical deps git clone && cd && horus build ``` `horus build` handles everything: 1. Reads `horus.toml` (your dependency declarations) 2. Reads `horus.lock` (pinned versions) — creates it if missing 3. Checks toolchain versions (Rust, Python) and warns on mismatches 4. Checks system dependencies via `pkg-config` and suggests install commands 5. Installs language packages (crates.io, PyPI, registry) 6. Compiles the project ## The Lockfile Format (v4) ```toml version = 4 config_hash = "sha256:a1b2c3..." [toolchain] rust = "1.78.0" python = "3.12.3" features = ["monitor"] [[package]] name = "horus_library" version = "0.1.9" source = "registry" checksum = "sha256:abc..." [[package]] name = "serde" version = "1.0.215" source = "crates.io" checksum = "sha256:def..." [[package]] name = "numpy" version = "1.26.4" source = "pypi" [[system]] name = "opencv" version = "4.8.1" pkg_config = "opencv4" apt = "libopencv-dev" brew = "opencv" pacman = "opencv" ``` ### Sections | Section | Purpose | |---------|---------| | `version` | Schema version (currently 4) | | `config_hash` | SHA-256 of `horus.toml` for staleness detection | | `[toolchain]` | Pinned Rust/Python/CMake versions | | `features` | Active feature flags at lock time | | `[[package]]` | Pinned package versions (registry, crates.io, PyPI) | | `[[system]]` | System dependencies with cross-platform package names | ## Cross-Platform System Dependencies Each `[[system]]` entry includes package names for multiple platforms: ```toml [[system]] name = "opencv" version = "4.8.1" pkg_config = "opencv4" # How to detect (cross-platform) apt = "libopencv-dev" # Debian/Ubuntu brew = "opencv" # macOS (Homebrew) pacman = "opencv" # Arch Linux choco = "opencv" # Windows (Chocolatey) ``` When `horus build` detects a missing system dependency, it prints the correct install command for your platform: ```bash # On Ubuntu: ✗ opencv — install with: sudo apt install -y libopencv-dev # On macOS: ✗ opencv — install with: brew install opencv # On Arch: ✗ opencv — install with: sudo pacman -S opencv ``` ## Commands ### `horus lock` Regenerate `horus.lock` from `horus.toml`: ```bash horus lock # [✓] Generated horus.lock v4 (12 packages) ``` ### `horus lock --check` Verify the lockfile is valid and check system dependencies: ```bash horus lock --check # [✓] horus.lock v4 is valid (12 packages, 2 system deps) # ⚠ Rust version mismatch: lockfile pins 1.78.0, you have 1.79.0 # ✗ opencv — install with: sudo apt install -y libopencv-dev ``` ### `horus build` Build the project. Automatically verifies the lockfile first: ```bash horus build # Lockfile verification: # ⚠ Python version mismatch: lockfile pins 3.12.3, you have 3.11.0 # [i] Building project in debug mode... ``` ## Workflows ### Team Development ```bash # Developer A: adds a dependency horus add numpy --source pypi horus build # updates horus.lock git add horus.toml horus.lock git commit -m "add numpy dependency" git push # Developer B: gets identical deps git pull horus build # reads horus.lock, installs exact versions ``` ### Robot Deployment ```bash # On dev machine: horus build --release scp -r . robot@192.168.1.5:~/project/ # On robot: cd ~/project horus build --release # horus.lock ensures identical deps ``` ### CI/CD ```yaml steps: - uses: actions/checkout@v4 - name: Install horus run: curl -sSf https://horusrobotics.dev/install.sh | sh - name: Build run: horus build --release - name: Test run: horus test ``` The lockfile in the repo ensures CI builds match local builds exactly. ## Version Checking The `[toolchain]` section records which Rust/Python versions were used when the lockfile was generated. On `horus build`, version mismatches produce warnings (not errors): - **Same major.minor** (e.g., 1.78.0 vs 1.78.5): no warning - **Different minor** (e.g., 1.78.0 vs 1.79.0): warning printed - **Different major** (e.g., 3.12 vs 2.7): warning printed Warnings don't block the build — they inform you that behavior may differ. ## Backward Compatibility - `horus.lock` v3 files (packages only) are still readable - Missing sections (`[toolchain]`, `[[system]]`, `features`) default to empty - On the next `horus lock` or `horus build`, the file is upgraded to v4 ## Best Practices 1. **Commit `horus.lock` to git** — this is the reproducibility mechanism 2. **Don't edit `horus.lock` manually** — use `horus lock` to regenerate 3. **Run `horus lock --check` in CI** — catch dependency drift early 4. **Update lockfile when adding deps** — `horus add` + `horus build` updates it automatically --- ## Configuration Reference Path: /package-management/configuration Description: Complete reference for horus.toml project config and .horus directory # Configuration Reference Complete guide to configuring HORUS projects via `horus.toml` and understanding the auto-managed `.horus/` directory. ## Quick Reference **Minimal `horus.toml`:** ```toml [package] name = "my_robot" version = "0.1.0" ``` **Full `horus.toml`:** ```toml [package] name = "my_robot_controller" version = "1.2.3" description = "Advanced mobile robot controller with navigation" authors = ["Robotics Team "] license = "Apache-2.0" edition = "1" repository = "https://github.com/team/my-robot-controller" package-type = "app" categories = ["control", "navigation"] [drivers] camera = "opencv" lidar = true enable = ["cuda", "editor"] [ignore] files = ["debug_*.rs", "**/temp/**"] directories = ["experiments/", "old/"] packages = ["ipython"] ``` ## Project Metadata All project metadata fields live under the `[package]` table. `horus.toml` is a config-only manifest -- it does not contain dependencies or language settings. Dependencies live in native build files (`Cargo.toml` for Rust, `pyproject.toml` for Python), and language is auto-detected from which build files exist. ### `name` (Required) Project name used for identification and package management. **Type**: String **Required**: Yes **Constraints**: - Must be unique within your organization - Use lowercase with hyphens (kebab-case) - No spaces or special characters except hyphens and underscores **Examples:** ```toml [package] name = "temperature-monitor" # or name = "mobile_robot_controller" # or name = "warehouse-navigation" ``` ### `version` (Required) Project version following semantic versioning. **Type**: String **Required**: Yes **Format**: `MAJOR.MINOR.PATCH` (semantic versioning) **Examples:** ```toml [package] version = "0.1.0" # Initial development # or version = "1.0.0" # First stable release # or version = "2.3.1" # Mature project ``` ### `edition` (Optional) Manifest schema version. Controls which fields and features are available in `horus.toml`. **Type**: String **Required**: No **Default**: `"1"` **Examples:** ```toml [package] edition = "1" ``` ### `description` (Optional) Human-readable description of your project. **Type**: String **Required**: No **Default**: None **Examples:** ```toml [package] description = "Temperature monitoring system with alerts" # or description = "Autonomous mobile robot for warehouse operations" ``` ### `authors` (Optional) Project authors list. **Type**: Array of strings **Required**: No **Default**: None **Examples:** ```toml [package] authors = ["Robotics Team "] # or authors = ["John Doe ", "Jane Smith "] ``` ### `license` (Optional) Project license identifier. **Type**: String **Required**: No **Default**: None **Common Values**: `Apache-2.0`, `MIT`, `GPL-3.0`, `BSD-3-Clause` **Examples:** ```toml [package] license = "Apache-2.0" # or license = "MIT" # or license = "Proprietary" ``` ### `repository` (Optional) URL to the project's source repository. **Type**: String **Required**: No **Default**: None **Examples:** ```toml [package] repository = "https://github.com/team/my-robot" ``` ### `package-type` (Optional) Classification of the package for registry discovery. **Type**: String **Required**: No **Default**: None **Values**: `node`, `driver`, `tool`, `algorithm`, `model`, `message`, `app` **Examples:** ```toml [package] package-type = "app" # or package-type = "node" # or package-type = "driver" ``` ### `categories` (Optional) Categories for package discovery in the registry. **Type**: Array of strings **Required**: No **Default**: None **Examples:** ```toml [package] categories = ["control", "navigation", "perception"] ``` ## Language Auto-Detection HORUS automatically detects the project language from native build files present in the project directory: | Build File | Detected Language | |------------|-------------------| | `Cargo.toml` | Rust | | `pyproject.toml` | Python | No `language` field is needed in `horus.toml`. When you run `horus new`, both `horus.toml` and the appropriate native build file (`Cargo.toml` or `pyproject.toml`) are created. ## Dependencies Dependencies are managed through **native build files**, not through `horus.toml`: | Language | Build File | Add Dependencies With | |----------|------------|----------------------| | **Rust** | `Cargo.toml` | `horus add ` (delegates to `cargo add`) | | **Python** | `pyproject.toml` | `horus add ` (delegates to `pip install`) | HORUS registry packages are also added with `horus add`, which updates the appropriate native build file. ### Rust Dependencies (Cargo.toml) For Rust projects, dependencies live in `Cargo.toml` alongside your `horus.toml`: ```toml # Cargo.toml (managed by cargo / horus add) [package] name = "my-robot" version = "0.1.0" edition = "2021" [dependencies] horus = { path = "..." } serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } ``` **Adding dependencies:** ```bash # Add a crate (delegates to cargo add) horus add serde --features derive horus add tokio --features full # Add from HORUS registry horus add pid-controller # Add with specific version horus add serde@1.0.200 ``` ### Python Dependencies (pyproject.toml) For Python projects, dependencies live in `pyproject.toml`: ```toml # pyproject.toml [project] name = "my-robot" version = "0.1.0" dependencies = [ "horus-robotics", "numpy>=1.24", "opencv-python>=4.8", ] ``` **Adding dependencies:** ```bash # Add a package (delegates to pip install) horus add numpy horus add opencv-python # Add from HORUS registry horus add pid-controller ``` ### HORUS Registry Packages HORUS registry packages (`horus add `) are added to the appropriate native build file based on the detected project language. ```bash # Install from HORUS registry horus add pid-controller horus add sensor-fusion horus add motion-planner ``` ## Ignore Patterns ### `ignore` (Optional) Exclude files and directories from HORUS processing. **Type**: Table with optional `files` and `directories` arrays **Required**: No **Default**: None ### `ignore.files` Exclude specific files from detection and execution. **Type**: Array of glob patterns **Patterns**: `*` (wildcard), `**/` (recursive directories) **Examples:** ```toml [ignore] files = [ "debug_*.py", # Ignore debug_test.py, debug_node.py "test_*.rs", # Ignore all test files "**/experiments/**", # Ignore files in any experiments/ directory "scratch.rs", # Ignore specific file ] ``` ### `ignore.directories` Exclude entire directories. **Type**: Array of directory names or paths **Examples:** ```toml [ignore] directories = ["old/", "experiments/", "tests/", "benchmarks/"] ``` ### `ignore.packages` Skip specific packages during auto-install. **Type**: Array of package name strings **Examples:** ```toml [ignore] packages = ["ipython", "debugpy"] ``` ### Complete Ignore Example ```toml [package] name = "robot_controller" version = "0.1.0" [ignore] # Don't run debug files files = ["debug_*.py", "test_*.rs", "**/temp/**"] # Don't process these directories directories = ["old_controllers/", "experiments/", "docs/"] ``` ## Feature Flags ### `enable` (Optional) Enable optional feature flags for your project. **Type**: Array of strings (at the top level, outside any table) **Examples:** ```toml enable = ["cuda", "editor"] ``` ## Hooks ### `[hooks]` (Optional) Declare commands that run automatically before or after `horus run`, `horus build`, and `horus test`. Eliminates manual `horus fmt && horus lint && horus run` pipelines. **Fields:** | Field | Type | Description | |-------|------|-------------| | `pre_run` | `string[]` | Run before `horus run` | | `pre_build` | `string[]` | Run before `horus build` | | `pre_test` | `string[]` | Run before `horus test` | | `post_test` | `string[]` | Run after `horus test` | **Built-in hook names:** `fmt`, `lint`, `check`. Any other name is looked up in `[scripts]`. **Example:** ```toml [hooks] pre_run = ["fmt", "lint"] # Auto-format and lint before every run pre_test = ["lint"] # Lint before testing post_test = ["clean --shm"] # Clean shared memory after tests (if defined in [scripts]) ``` **Skipping hooks:** ```bash horus run --no-hooks # Skip all hooks for this run horus test --no-hooks # Skip hooks for this test ``` **How it works:** When you run `horus run`, the scheduler checks `[hooks].pre_run`. For each entry, it calls the corresponding command function (`fmt` → `horus fmt`, `lint` → `horus lint`). If any hook fails, the run is aborted. Use `--no-hooks` to bypass when debugging. --- ## Driver Configuration ### `[drivers]` (Optional) Configure hardware drivers. Three forms supported: **Simple** — enable a driver or specify a backend (feature flags only): ```toml [drivers] camera = "opencv" lidar = true ``` **Terra** — connect to hardware via the Terra HAL. Auto-adds the correct Terra crate to your build: ```toml [drivers.arm] terra = "dynamixel" port = "/dev/ttyUSB0" baudrate = 1000000 servo_ids = [1, 2, 3, 4, 5, 6] [drivers.lidar] terra = "rplidar" port = "/dev/ttyUSB1" [drivers.imu] terra = "bno055" bus = "i2c-1" address = 0x28 ``` **Registry package** — use a community or vendor driver package: ```toml [drivers.force_sensor] package = "horus-driver-ati-netft" address = "192.168.1.100" filter_hz = 500 ``` **Local code** — reference your own driver registered via `register_driver!`: ```toml [drivers.conveyor] node = "ConveyorDriver" port = "/dev/ttyACM0" baudrate = 57600 ``` All forms can be mixed in the same file. The `terra`, `package`, and `node` keys determine the driver source. All other keys become params passed to the driver at runtime. **Supported Terra drivers:** | Name | Crate | Hardware | |------|-------|----------| | `dynamixel` | terra-serial | Dynamixel servos | | `rplidar` | terra-serial | SLAMTEC RPLiDAR | | `vesc` | terra-serial | VESC motor controllers | | `modbus` | terra-serial | Modbus RTU devices | | `realsense` | terra-realsense | Intel RealSense cameras | | `webcam` | terra-webcam | V4L2 cameras | | `velodyne`, `ouster`, `livox` | terra-lidar | 3D LiDAR | | `mpu6050`, `bno055` | terra-embedded | I2C IMU sensors | | `odrive`, `canopen` | terra-can | CAN bus devices | | `ethercat` | terra-ethercat | EtherCAT servos/PLCs | | `i2c`, `spi`, `serial`, `can`, `gpio`, `pwm` | terra-{bus} | Raw bus access | | `virtual` | terra-virtual | Mock devices for testing | See the [Driver API reference](/rust/api/drivers) for the runtime API. ## The `.horus/` Directory The `.horus/` directory is **automatically managed** by HORUS. You should never manually edit files inside it. ### Structure ``` my_project/ ├── horus.toml # Project config (edit this) ├── Cargo.toml # Rust dependencies (edit this) ├── main.rs # Your code (edit this) └── .horus/ # Build cache (don't touch) ├── target/ # Build artifacts └── packages/ # Cached registry packages ``` For Python projects: ``` my_project/ ├── horus.toml # Project config (edit this) ├── pyproject.toml # Python dependencies (edit this) ├── main.py # Your code (edit this) └── .horus/ # Build cache (don't touch) └── packages/ # Cached registry packages ``` ### What's Inside `.horus/` `.horus/` is a build cache only: **`target/`** (Rust projects): - Cargo build artifacts - Can be large (ignore in git) **`packages/`**: - Cached HORUS registry packages - Symlinks to global cache when possible ### Git Configuration **Always add to `.gitignore`:** ```gitignore # HORUS build cache .horus/ ``` The `horus new` command automatically creates this `.gitignore` for you. ### When `.horus/` is Created `.horus/` is created automatically when you run: - `horus run` - `horus install` - `horus build` You never need to create it manually. ### Cleaning `.horus/` **Remove local environment:** ```bash rm -rf .horus/ ``` **Regenerate on next run:** ```bash horus run # Automatically recreates .horus/ ``` ## Complete Examples ### Rust Application **horus.toml:** ```toml [package] name = "temperature-monitor" version = "0.1.0" description = "Simple temperature monitoring system" authors = ["Robotics Team"] license = "Apache-2.0" [ignore] files = ["debug_*.rs"] ``` **Cargo.toml** (alongside horus.toml): ```toml [package] name = "temperature-monitor" version = "0.1.0" edition = "2021" [dependencies] horus = { path = "..." } serde = { version = "1", features = ["derive"] } ``` ### Rust Application with Drivers **horus.toml:** ```toml [package] name = "robot-backend" version = "1.0.0" description = "Robot control backend with sensor processing" authors = ["ACME Robotics"] license = "Apache-2.0" [drivers] camera = "opencv" lidar = true enable = ["cuda"] [ignore] directories = ["tests/"] ``` **Cargo.toml:** ```toml [package] name = "robot-backend" version = "1.0.0" edition = "2021" [dependencies] horus = { path = "..." } serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } [dev-dependencies] criterion = "0.5" ``` > **Note**: HORUS uses a flat namespace (like ROS), so multiple processes automatically share topics when using the same topic names. No configuration needed! ### Python Application **horus.toml:** ```toml [package] name = "vision-processor" version = "0.2.0" description = "Computer vision processing node" authors = ["Vision Team"] license = "MIT" [ignore] directories = ["notebooks/", "experiments/"] ``` **pyproject.toml** (alongside horus.toml): ```toml [project] name = "vision-processor" version = "0.2.0" dependencies = [ "horus-robotics", "numpy>=1.24", "opencv-python>=4.8", "pillow", ] ``` ## Best Practices ### Version Your Configuration Always commit `horus.toml` and your native build file to version control: ```bash git add horus.toml Cargo.toml # Rust project git add horus.toml pyproject.toml # Python project ``` ### Use Semantic Versioning Follow semver for your project version: - `0.x.y` - Initial development - `1.0.0` - First stable release - `x.y.z` - Major.Minor.Patch ### Keep horus.toml Minimal `horus.toml` is config only. Dependencies go in native build files: ```toml # Good (minimal horus.toml) [package] name = "my_robot" version = "0.1.0" ``` Add optional fields only when needed: ```toml # With optional metadata [package] name = "my_robot" version = "0.1.0" description = "A temperature monitoring robot" authors = ["Team"] license = "MIT" ``` ## Validation HORUS validates `horus.toml` on every command. Common errors: ### Missing Required Fields ``` Error: Missing required field 'name' in horus.toml ``` **Fix**: Add `name` and `version` fields under `[package]`. ### Invalid TOML Syntax ``` Error: Failed to parse horus.toml: invalid TOML syntax at line 5 ``` **Fix**: Check TOML syntax (key-value pairs use `=`, strings must be quoted, tables use `[brackets]`). ### Invalid Version Format ``` Error: Invalid version format: '1.0'. Expected semantic version (e.g., '1.0.0') ``` **Fix**: Use format `"MAJOR.MINOR.PATCH"`. ## Next Steps - **[Package Management](/package-management/package-management)** - Install and manage packages - **[CLI Reference](/development/cli-reference)** - All HORUS commands - **[Topic](/concepts/core-concepts-topic)** - Understanding the IPC architecture ======================================== # SECTION: Performance ======================================== --- ## Performance Optimization Path: /performance/performance Description: Get maximum performance from HORUS # Performance Optimization HORUS is already fast by default. This guide helps you squeeze out extra performance when needed. ## Cross-Platform Philosophy HORUS is designed for **development on any OS** with **production deployment on Linux**: | Phase | Supported Platforms | Performance | |-------|---------------------|-------------| | **Development** | Windows, macOS, Linux | Good (standard IPC) | | **Testing** | Windows, macOS, Linux | Good (standard IPC) | | **Production** | Linux (recommended) | Best (sub-100ns with RT) | All performance features use **graceful degradation** - your code runs everywhere, with maximum performance on Linux. Advanced features like RtConfig (SCHED_FIFO, mlockall) and SIMD acceleration automatically fall back to safe defaults on unsupported platforms. ## Why HORUS is Fast ### Shared Memory Architecture **Zero network overhead**: Data written to shared memory, read directly by subscribers HORUS automatically selects the optimal shared memory backend for your platform (Linux, macOS, Windows). No configuration needed. **Zero serialization**: Fixed-size structs copied directly to shared memory **Zero-copy loan pattern**: Publishers write directly to shared memory slots ### Optimized Data Structures HORUS uses carefully optimized memory layouts to minimize latency. The communication paths are designed for maximum throughput with predictable timing — single-producer paths achieve ~87ns, multi-producer paths achieve ~313ns. ## Benchmark Results For detailed benchmark methodology, raw data, and comprehensive latency/throughput tables, see the dedicated **[Benchmarks](/performance/benchmarks)** page. **Headlines:** 87ns send latency (SPSC), 230-575x faster than ROS2 DDS, 12M+ msg/sec throughput for small messages. ### Throughput HORUS can handle: - **12M+ messages/second** for small messages (16B) with SPSC Topic - **3M+ messages/second** for small messages (16B) with MPMC Topic - **1M+ messages/second** for medium messages (1KB) - **100K+ messages/second** for large messages (100KB) ## Build Optimization ### Always Use Release Mode Debug builds are **10-100x slower**: ```bash # SLOW: Debug build horus run # FAST: Release build horus run --release ``` **Why it matters**: - Debug: 50µs per tick - Release: 500ns per tick - **100x difference** for the same code ### Link-Time Optimization (LTO) Enable LTO in your `Cargo.toml` for additional 10-20% speedup: ```toml # Cargo.toml [profile.release] opt-level = 3 lto = "fat" codegen-units = 1 ``` **Warning**: Slower compilation, but faster execution. ### Target CPU Features **CPU-Specific Optimizations:** HORUS compiles with Rust compiler optimizations enabled in release mode. For advanced CPU-specific tuning, the framework is optimized for modern x86-64 and ARM64 processors. **Gains**: 5-15% from CPU-specific SIMD instructions (automatically enabled in release builds). ### Hardware Acceleration HORUS automatically uses hardware-accelerated memory operations when available (e.g., SIMD on x86_64). No configuration needed — your code runs on any platform, with extra performance on supported hardware. For maximum performance, compile targeting your specific CPU: ```bash RUSTFLAGS="-C target-cpu=native" cargo build --release ``` ## Message Optimization ### Use Fixed-Size Types ```rust // FAST: Fixed-size array pub struct LaserScan { pub ranges: [f32; 360], // Stack-allocated } // SLOW: Dynamic vector pub struct BadLaserScan { pub ranges: Vec, // Heap-allocated } ``` **Impact**: Fixed-size avoids heap allocations in hot path. ### Choose Typed Messages Over Generic ```rust // FAST: Small, fixed-size struct let topic: Topic = Topic::new("pose")?; topic.send(Pose2D { x: 1.0, y: 2.0, theta: 0.5 }); // IPC latency: ~87-313ns depending on topology // SLOWER: Larger struct with more data let topic: Topic = Topic::new("sensors")?; // Latency scales linearly with message size ``` **Rule**: Use the smallest struct that represents your data. Avoid padding and unused fields. ### Choose Appropriate Precision ```rust // f32 (single precision) - sufficient for most robotics pub struct FastPose { pub x: f32, // 4 bytes pub y: f32, // 4 bytes } // f64 (double precision) - scientific applications pub struct PrecisePose { pub x: f64, // 8 bytes pub y: f64, // 8 bytes } ``` **Rule**: Use `f32` unless you need scientific precision. ### Minimize Message Size ```rust // GOOD: 8 bytes struct CompactCmd { linear: f32, // 4 bytes angular: f32, // 4 bytes } // BAD: 1KB+ bytes struct BloatedCmd { linear: f32, angular: f32, metadata: [u8; 256], // Unused debug_info: [u8; 768], // Unused } ``` **Every byte matters**: Latency scales with message size. ### Batch Small Messages Instead of sending 100 separate f32 values: ```rust // SLOW: 100 separate messages for value in values { topic.send(value); // 100 IPC operations } // FAST: One batched message pub struct BatchedData { values: [f32; 100], } topic.send(batched); // 1 IPC operation ``` **Speedup**: 50-100x for batched operations. ## Node Optimization ### Keep tick() Fast Target: **<1ms per tick** for real-time control. ```rust // GOOD: Fast tick fn tick(&mut self) { let data = self.read_sensor(); // Quick read self.process_pub.send(data); // ~500ns } // BAD: Slow tick fn tick(&mut self) { let data = std::fs::read_to_string("config.yaml").unwrap(); // 1-10ms! // ... } ``` **File I/O, network calls, sleeps = slow**. Do these in `init()` or separate threads. ### Pre-Allocate in init() ```rust fn init(&mut self) -> Result<()> { // Pre-allocate buffers self.buffer = vec![0.0; 10000]; // Open connections self.device = Device::open()?; // Load configuration self.config = Config::from_file("config.yaml")?; Ok(()) } fn tick(&mut self) { // Use pre-allocated resources - no allocations here! self.buffer[0] = self.device.read(); } ``` **Allocations in tick() = slow**. Move to `init()`. ### Avoid Unnecessary Cloning ```rust // BAD: Unnecessary clone fn tick(&mut self) { if let Some(data) = self.sub.recv() { let copy = data.clone(); // Unnecessary! self.process(copy); } } // GOOD: Direct use fn tick(&mut self) { if let Some(data) = self.sub.recv() { self.process(data); // Already cloned by recv() } } ``` `Topic::recv()` already clones data. Don't clone again. ### Minimize Logging ```rust // BAD: Logging every tick fn tick(&mut self) { hlog!(debug, "Tick #{}", self.counter); // Slow! self.counter += 1; } // GOOD: Conditional logging fn tick(&mut self) { if self.counter % 1000 == 0 { // Log every 1000 ticks hlog!(info, "Reached tick #{}", self.counter); } self.counter += 1; } ``` **Logging is expensive**. Log sparingly in hot paths. ## Scheduler Optimization ### Understanding Tick Rate The default scheduler runs at 100 Hz (10ms per tick). Use `.tick_rate()` to change it: ```rust // Default: 100 Hz let scheduler = Scheduler::new(); // 10kHz for high-performance control loops let scheduler = Scheduler::new().tick_rate(10000_u64.hz()); ``` **Key Point**: Keep individual node tick() methods fast (ideally <1ms) to maintain the target tick rate. ### Use Priority Levels ```rust // Critical tasks run first (order 0 = highest) scheduler.add(safety).order(0).build()?; // Logging runs last (order 100 = lowest) scheduler.add(logger).order(100).build()?; ``` **Predictable execution order** = better performance. Use lower numbers for higher priority tasks. ### Minimize Node Count ```rust // BAD: 50 small nodes for i in 0..50 { scheduler.add(TinyNode::new(i)).order(50).build()?; } // GOOD: One aggregated node scheduler.add(AggregatedNode::new()).order(50).build()?; ``` **Fewer nodes** = less scheduling overhead. ## Ultra-Low-Latency Networking (Linux) HORUS provides optional kernel bypass networking for sub-microsecond latency requirements. ### Transport Options | Transport | Latency (send-only) | Throughput | Requirements | |-----------|---------|------------|--------------| | Shared Memory (Topic, 1:1) | ~87ns | 12M+ msg/s | Local only | | Shared Memory (Topic, many:many) | ~313ns | 3M+ msg/s | Local only | | io_uring | 2-3µs | 500K+ msg/s | Linux 5.1+ | | Batch UDP | 3-5µs | 300K+ msg/s | Linux 3.0+ | | Standard UDP | 5-10µs | 200K+ msg/s | Cross-platform | ### Enable io_uring Transport io_uring eliminates syscalls on the send path using kernel-side polling: ```bash # Build with io_uring support (Cargo feature flag) cargo build --release --features io-uring-net ``` **Requirements:** - Linux 5.1+ (5.6+ recommended for SQ polling) - CAP_SYS_NICE capability for SQ_POLL mode ### Enable Batch UDP (Linux) Batch UDP uses `sendmmsg`/`recvmmsg` syscalls for efficient batched network I/O: ```bash # Batch UDP is automatically enabled on Linux - no extra dependencies needed cargo build --release ``` **Requirements:** - Linux 3.0+ (available on virtually all modern Linux systems) ### Enable All Ultra-Low-Latency Features ```bash # Build with all ultra-low-latency features (io_uring) cargo build --release --features ultra-low-latency ``` ### Smart Transport Selection For network topics, HORUS automatically selects the best transport based on available system features and kernel version. Configure network endpoints through topic configuration rather than the `Topic::new()` API (which creates local shared memory topics). See [Network Backends](/advanced/network-backends) for details. ## Shared Memory Optimization ### Check Available Space ```bash df -h /dev/shm ``` **Insufficient space** = message drops. ### Increase /dev/shm Size ```bash # Increase to 4GB sudo mount -o remount,size=4G /dev/shm ``` **More space** = larger buffer capacity. ### Clean Up Stale Topics **Note**: HORUS automatically cleans up sessions after each run. Manual cleanup is rarely needed. ```bash # Clean all HORUS shared memory (if needed after crashes) horus clean --shm ``` **Stale topics** from crashes can waste space, but auto-cleanup prevents this in normal operation. ### Topic Memory Usage Topics use shared memory slots proportional to message size. Keep messages small to reduce memory footprint: ```rust // Small messages use less shared memory let cmd: Topic = Topic::new("cmd_vel")?; // 16B per slot // Large messages use more shared memory let cloud: Topic = Topic::new("cloud")?; // 120KB per slot ``` **Balance**: Message size directly affects shared memory consumption. ## Profiling and Measurement ### Built-In Metrics HORUS automatically tracks node performance metrics. Use `horus monitor` to view real-time performance data including tick duration, messages sent, and CPU usage. **Available metrics** (on `NodeMetrics`): - `total_ticks`: Total number of ticks - `avg_tick_duration_ms`: Average tick time in milliseconds - `max_tick_duration_ms`: Worst-case tick time in milliseconds - `messages_sent`: Messages published - `messages_received`: Messages received - `errors_count`: Total error count - `uptime_seconds`: Node uptime in seconds ### IPC Latency Logging HORUS automatically tracks IPC timing for each topic operation. The `horus monitor` web interface displays per-log-entry metrics: ``` Tick: 12μs | IPC: 296ns ``` Each log entry includes `tick_us` (node tick time in microseconds) and `ipc_ns` (IPC write time in nanoseconds). ### Manual Profiling ```rust use std::time::Instant; fn tick(&mut self) { let start = Instant::now(); self.expensive_operation(); let duration = start.elapsed(); println!("Operation took: {:?}", duration); } ``` ### CPU Profiling Use `perf` on Linux: ```bash # Profile your application perf record --call-graph dwarf horus run --release # View results perf report ``` **Hotspots** show where CPU time is spent. ## Common Performance Pitfalls ### Pitfall: Using Debug Builds ```bash # SLOW: 50µs/tick horus run # FAST: 500ns/tick horus run --release ``` **Fix**: Always use `--release` for benchmarks and production. ### Pitfall: Allocations in tick() ```rust // BAD fn tick(&mut self) { let buffer = vec![0.0; 1000]; // Heap allocation every tick! } // GOOD struct Node { buffer: Vec, // Pre-allocated } fn init(&mut self) -> Result<()> { self.buffer = vec![0.0; 1000]; // Allocate once Ok(()) } ``` **Fix**: Pre-allocate in `init()`. ### Pitfall: Excessive Logging ```rust // BAD: 60 logs per second fn tick(&mut self) { hlog!(debug, "Tick"); // Every 16ms! } // GOOD: 1 log per second fn tick(&mut self) { self.tick_count += 1; if self.tick_count % 60 == 0 { hlog!(info, "60 ticks completed"); } } ``` **Fix**: Log sparingly. ### Pitfall: Large Message Types ```rust // BAD: 1MB per message pub struct HugeMessage { image: [u8; 1_000_000], } // GOOD: Compressed or separate channel pub struct CompressedImage { data: Vec, // JPEG compressed, ~50KB } ``` **Fix**: Compress or split large data. ### Pitfall: Synchronous I/O in tick() ```rust // BAD: Blocking I/O fn tick(&mut self) { let data = std::fs::read("data.txt").unwrap(); // Blocks! } // GOOD: Async or pre-loaded fn init(&mut self) -> Result<()> { self.data = std::fs::read("data.txt")?; // Load once Ok(()) } ``` **Fix**: Move I/O to `init()` or use async. ## Performance Checklist Before deployment, verify: - [ ] Build in release mode (`--release`) - [ ] Profile with `perf` or similar - [ ] tick() completes in <1ms - [ ] No allocations in tick() - [ ] Messages use fixed-size types - [ ] Logging is rate-limited - [ ] Shared memory has sufficient space - [ ] IPC latency is <10µs - [ ] Priority levels set correctly ## Measuring Your Performance ### Latency Measurement ```rust use std::time::Instant; struct BenchmarkNode { pub_topic: Topic, sub_topic: Topic, start_time: Option, } impl Node for BenchmarkNode { fn tick(&mut self) { // Publish self.start_time = Some(Instant::now()); self.pub_topic.send(42.0); // Receive if let Some(data) = self.sub_topic.recv() { if let Some(start) = self.start_time { let latency = start.elapsed(); println!("Round-trip latency: {:?}", latency); } } } } ``` ### Throughput Measurement ```rust struct ThroughputTest { pub_topic: Topic, message_count: u64, start_time: Instant, } impl Node for ThroughputTest { fn tick(&mut self) { for _ in 0..1000 { self.pub_topic.send(42.0); self.message_count += 1; } if self.message_count % 100_000 == 0 { let elapsed = self.start_time.elapsed().as_secs_f64(); let throughput = self.message_count as f64 / elapsed; println!("Throughput: {:.0} msg/s", throughput); } } } ``` ## Real-Time Configuration For hard real-time applications requiring bounded latency, HORUS provides system-level RT configuration: ```rust use horus::prelude::*; // Configure for hard real-time operation let config = RtConfig::hard_realtime(Some(&[2, 3])); // Pin to isolated cores config.apply()?; // This enables: // - mlockall() - No page faults // - SCHED_FIFO priority 80 - Preempts normal processes // - CPU affinity - No migration jitter // - Stack prefaulting - No lazy allocation ``` For detailed configuration options, see the [Scheduler Configuration](/advanced/scheduler-configuration). ## Next Steps - Apply these optimizations to your [Examples](/rust/examples/basic-examples) - Configure [Scheduler Settings](/advanced/scheduler-configuration) for bounded latency - Learn about [Multi-Language Support](/concepts/multi-language) - Read the [Core Concepts](/concepts/core-concepts-nodes) for deeper understanding - Check the [CLI Reference](/development/cli-reference) for build options --- ## Benchmarks Path: /performance/benchmarks Description: Performance testing with real robotics workloads # HORUS Benchmarks Performance validation with real-world robotics workloads. ## Benchmark Methodology ### Measurement Approach - **Statistical sampling**: Criterion.rs with 20+ samples per measurement - **Confidence intervals**: Min/mean/max with outlier detection - **Controlled methodology**: 1s warm-up, 5s measurement phases - **Reproducible**: Less than 1% variance across measurements - **Comprehensive coverage**: 5 workload types, 4 scalability points ### Workload Testing - **Real workloads**: Control loops, sensor fusion, I/O operations - **Fault injection**: Failure policy recovery testing - **Scale testing**: Validated up to 200 concurrent nodes - **Mixed patterns**: Combined blocking/non-blocking operations - **Long-running**: 25+ second failure recovery tests ## Executive Summary **HORUS delivers sub-microsecond to low-microsecond latency for production robotics applications:** | Message Type | Size | Latency (Topic N:N) | Throughput | Typical Rate | Headroom | |--------------|------|---------------|------------|--------------|----------| | **CmdVel** | 16 B | **~500 ns** | 2.7M msg/s | 1000 Hz | 2,700x | | **BatteryState** | 104 B | **~600 ns** | 1.67M msg/s | 1 Hz | 1.67M x | | **IMU** | 304 B | **~940 ns** | 1.8M msg/s | 100 Hz | 18,000x | | **Odometry** | 736 B | **~1.1 μs** | 1.3M msg/s | 50 Hz | 26,000x | | **LaserScan** | 1.5 KB | **~2.2 μs** | 633K msg/s | 10 Hz | 63,300x | | **PointCloud (1K)** | ~12 KB | **~12 μs** | 83K msg/s | 30 Hz | 2,767x | | **PointCloud (10K)** | ~120 KB | **~360 μs** | 4.7K msg/s | 30 Hz | 157x | --- ## Performance Highlights ### Key Findings **Sub-microsecond latency** for messages up to 1.5KB **Serde integration** works flawlessly with complex nested structs **Linear scaling** with message size (predictable performance) **Massive headroom** for all typical robotics frequencies ### Production Readiness - **Real-time control**: ~500 ns latency supports 1000Hz+ control loops with 2,700x headroom - **Sensor fusion**: Mixed workload maintains sub-microsecond performance (648 ns avg) - **Perception pipelines**: 10K point clouds @ 30Hz with 189x headroom - **Multi-robot systems**: Throughput supports 100+ robots on single node --- ## Detailed Results ### CmdVel (Motor Control Command) **Use Case**: Real-time motor control @ 1000Hz **Structure**: `{ timestamp: u64, linear: f32, angular: f32 }` ``` Average Latency: ~500 ns (Topic N:N) Throughput: 2.7M msg/s Topic 1:1: ~85 ns median ``` **Analysis**: Sub-microsecond performance suitable for 1000Hz control loops with 2,700x headroom. --- ### LaserScan (2D Lidar Data) **Use Case**: 2D lidar sensor data @ 10Hz **Structure**: `{ ranges: [f32; 360], angle_min/max, metadata }` ``` Average Latency: ~2.2 μs (Topic N:N) Throughput: 633K msg/s Topic 1:1: ~900 ns estimated ``` **Analysis**: Consistent low-microsecond latency for 1.5KB messages. Can easily handle 10Hz lidar updates with 63,300x headroom. --- ### IMU (Inertial Measurement Unit) **Use Case**: Orientation and acceleration @ 100Hz **Structure**: `{ orientation: [f64; 4], angular_velocity: [f64; 3], linear_acceleration: [f64; 3], covariances: [f64; 27] }` ``` Average Latency: ~940 ns (Topic N:N) Throughput: 1.8M msg/s Topic 1:1: ~400 ns estimated ``` **Analysis**: Sub-microsecond performance with complex nested arrays and 27-element covariance matrices. --- ### Odometry (Pose + Velocity) **Use Case**: Robot localization @ 50Hz **Structure**: `{ pose: Pose2D, twist: Twist, pose_covariance: [f64; 36], twist_covariance: [f64; 36] }` ``` Average Latency: ~1.1 μs (Topic N:N) Throughput: 1.3M msg/s Topic 1:1: ~600 ns estimated ``` **Analysis**: Low-microsecond latency for 736-byte messages with extensive covariance data. --- ### PointCloud (3D Perception) #### Small (100 points @ 30Hz) ``` Average Latency: 1.85 μs Throughput: 539,529 msg/s Data Size: ~1.2 KB ``` #### Medium (1,000 points @ 30Hz) ``` Average Latency: 7.55 μs Throughput: 132,432 msg/s Data Size: ~12 KB ``` #### Large (10,000 points @ 30Hz) ``` Average Latency: ~360 μs (Topic N:N) Throughput: 4.7K msg/s Data Size: ~120 KB ``` **Analysis**: Linear scaling with point count. Even 10K point clouds process in ~360 μs (sufficient for 30Hz perception with 157x headroom). --- ### Mixed Workload (Realistic Robot Loop) **Simulation**: Real robot control loop @ 100Hz **Components**: CmdVel @ 100Hz + IMU @ 100Hz + BatteryState @ 1Hz ``` Total Operations: 20,100 messages Average Latency: ~1.0 μs (Topic N:N) Throughput: ~1.5M msg/s Range: ~500-1200 ns ``` **Analysis**: Low-microsecond average latency for mixed message types simulating realistic robotics workload. --- ## Comparison with traditional frameworks ### Latency Comparison > **Measurement Note**: Topic 1:1 values below are **send-only** (one-direction). For round-trip (send+receive), approximately double these values (e.g., 87ns send-only → ~175ns round-trip). | Framework | Small Msg (send-only) | Medium Msg (send-only) | Large Msg (send-only) | |-----------|-----------|------------|-----------| | **HORUS Topic (1:1)** | **87 ns** | **~160 ns** | **~400 ns** | | **HORUS Topic (N:N)** | **313 ns** | **~500 ns** | **~1.1 μs** | | ROS2 (DDS) | 50-100 μs | 100-500 μs | 1-10 ms | | ROS2 (FastDDS) | 20-50 μs | 50-200 μs | 500 μs - 5 ms | **Performance Advantage**: HORUS is **230-575x faster** than ROS2 for typical message sizes. --- ## Latency by Message Size > **Measurement Note**: All latencies below are **send-only** (one-direction publish). "1:1" = single producer/consumer, "N:N" = multiple producers and consumers. | Message Size | Message Type | N:N (send-only) | 1:1 (send-only) | vs ROS2 | |-------------|--------------|-------------|--------------|---------| | 16 B | CmdVel | ~313 ns | 87 ns | **230-575x faster** | | 104 B | BatteryState | ~600 ns | ~350 ns | **83-286x faster** | | 304 B | IMU | ~940 ns | ~400 ns | **53-250x faster** | | 736 B | Odometry | ~1.1 μs | ~600 ns | **45-167x faster** | | 1,480 B | LaserScan | ~2.2 μs | ~900 ns | **23-111x faster** | **Observation**: Near-linear scaling with message size demonstrates efficient serialization and IPC. --- ## Python Performance The HORUS Python bindings (PyO3) call directly into the Rust shared memory layer, avoiding pickle serialization overhead. Python nodes and Rust nodes communicate through the same shared memory, enabling cross-language interoperability with minimal overhead. **Why Python HORUS is Fast:** 1. **Zero-copy via Rust core**: Python bindings call directly into Rust shared memory 2. **No pickle overhead**: Messages use efficient binary serialization 3. **PyO3 efficiency**: Minimal FFI overhead between Python and Rust ### TensorPool HORUS TensorPool provides shared memory tensors optimized for ML/AI workloads. Pre-mapped shared memory means no `malloc()` or zero-initialization on the hot path. ```python from horus import TensorPool import numpy as np # Create pool pool = TensorPool(12345) # pool_id # Allocate tensor (pre-mapped shared memory) h = pool.alloc([1024, 1024], 'float32') # Zero-copy NumPy view arr = h.numpy() # No data copied # Cross-process sharing via shared memory descriptor = h.to_descriptor() ``` **Key Advantages:** - **Cross-process sharing** via shared memory - **Pre-allocated pool** — no malloc on hot path - **Refcounted handles** — safe concurrent access - **Zero-copy NumPy** — `.numpy()` returns view --- ## Running Rust Benchmarks ### Quick Run ```bash cd horus cargo run --release -p horus_benchmarks --bin robotics_messages_benchmark ``` ### Available Benchmarks | Binary | Description | |--------|-------------| | `robotics_messages_benchmark` | IPC latency with real robotics message types | | `all_paths_latency` | AdaptiveTopic latency across all backend routes | | `cross_process_benchmark` | Cross-process shared memory IPC | | `scalability_benchmark` | Scaling with producer/consumer thread counts | | `determinism_benchmark` | Execution determinism and jitter | | `dds_comparison_benchmark` | Comparison with DDS middleware (requires `--features dds`) | Run any benchmark with: ```bash cargo run --release -p horus_benchmarks --bin # JSON output for CI/regression tracking cargo run --release -p horus_benchmarks --bin -- --json results.json ``` Criterion micro-benchmarks: ```bash cd horus cargo bench -p horus_benchmarks ``` ### Expected Output ``` HORUS Production Message Benchmark Suite Testing with real robotics message types CmdVel (Motor Control Command) Size: 16 bytes | Typical rate: 1000Hz Latency (avg): ~500 ns (Topic N:N) / ~85 ns (Topic 1:1) Throughput: 2.7M msg/s (Topic N:N) LaserScan (2D Lidar Data) Size: 1480 bytes | Typical rate: 10Hz Latency (avg): ~2.2 μs (Topic N:N) / ~900 ns (Topic 1:1) Throughput: 633K msg/s (Topic N:N) ``` --- ## Use Case Selection ### Message Type Guidelines **CmdVel (~500 ns N:N / ~85 ns 1:1)** - Motor control @ 1000Hz - Real-time actuation commands - Safety-critical control loops **IMU (~940 ns N:N / ~400 ns 1:1)** - High-frequency sensor fusion @ 100Hz - State estimation pipelines - Orientation tracking **LaserScan (~2.2 μs N:N / ~900 ns 1:1)** - 2D lidar @ 10Hz - Obstacle detection - SLAM front-end **Odometry (~1.1 μs N:N / ~600 ns 1:1)** - Pose estimation @ 50Hz - Dead reckoning - Filter updates **PointCloud (~360 μs for 10K pts)** - 3D perception @ 30Hz - Object detection pipelines - Dense mapping --- ## Performance Characteristics ### Strengths 1. **Sub-microsecond latency** for messages up to 1.5KB 2. **Consistent performance** across message types (low variance) 3. **Linear scaling** with message size 4. **Production-ready** throughput with large headroom 5. **Serde integration** handles complex nested structs efficiently ### Additional Notes - **Complex structs** (IMU with 27-element covariances): Still sub-microsecond - **Variable-size messages** (PointCloud with Vec): Linear scaling --- ## Real-World Applications | Application | Frequency | HORUS (Topic 1:1) | HORUS (Topic N:N) | ROS2 | Speedup | |-------------|-----------|--------------|-------------|------|---------| | Motor control | 1000 Hz | ~85 ns | ~500 ns | 50 μs | **200-588x** | | IMU fusion | 100 Hz | ~400 ns | ~940 ns | 50 μs | **53-125x** | | Lidar SLAM | 10 Hz | ~900 ns | ~2.2 μs | 100 μs | **45-111x** | | Vision | 30 Hz | ~120 μs | ~360 μs | 5 ms | **14-42x** | | Planning | 100 Hz | ~600 ns | ~1.1 μs | 100 μs | **91-167x** | --- ## Methodology ### Benchmark Pattern: Ping-Pong **HORUS uses the industry-standard ping-pong benchmark pattern for IPC latency measurement:** >C: 1. Send message with RDTSC Note right of C: 2. Read RDTSC, calc latency C->>P: 3. Send ACK Note left of P: 4. Wait for ACK P->>C: 5. Send next message `} caption="Ping-Pong Benchmark Pattern" /> **Why Ping-Pong?** - **Industry standard**: Used by ROS2, iceoryx2, ZeroMQ benchmarks - **Prevents queue buildup**: Each message acknowledged before next send - **Realistic**: Models request-response patterns in robotics - **Comparable**: Direct apples-to-apples comparison with other frameworks - **Conservative**: Measures true round-trip latency, not just one-way send **What we measure:** - Round-trip time: Producer Consumer ACK Producer - Includes serialization, IPC, deserialization, and synchronization - Cross-core communication (Core 0 ↔ Core 1) **What we DON'T measure:** - Burst throughput (no backpressure) - One-way send time without acknowledgment - Same-core communication (unrealistic for multi-process IPC) ### Test Environment - **Build**: `cargo build --release` with full optimizations - **CPU Governor**: Performance mode - **CPU Affinity**: Producer pinned to Core 0, Consumer pinned to Core 1 - **Process Isolation**: Dedicated topics per benchmark - **Warmup**: 1,000 iterations before measurement - **Measurement**: RDTSC (cycle-accurate timestamps) ### Message Realism - Actual HORUS library message types - Serde serialization (production path) - Realistic field values and sizes - Complex nested structures (IMU, Odometry) ### Statistical Methodology - 10,000 iterations per test - Median, P95, P99 latency tracking - Variance tracking (min/max ranges) - Multiple message sizes - Mixed workload testing ### Measurement Details **RDTSC Calibration:** - Null cost (back-to-back rdtsc): ~36 cycles - Target on modern x86_64: 20-30 cycles - Timestamp embedded directly in message payload **Cross-Core Testing:** - Producer and consumer on different CPU cores - Simulates real multi-process robotics systems - Includes cache coherency overhead (~60 cycles theoretical minimum) --- ## Scheduler Performance ### Enhanced Smart Scheduler HORUS now includes an intelligent scheduler that automatically optimizes node execution based on runtime behavior: **Key Enhancements:** - **Tiered Execution**: Explicit tier annotation (UltraFast, Fast, Normal) - **Failure Policies**: Per-node failure handling with automatic recovery - **Predictable by Default**: Sequential execution with consistent priority ordering - **Safety Monitoring**: WCET enforcement, watchdogs, and emergency stop ### Comprehensive Benchmark Results **Test Configuration:** - Workload duration: 5 seconds per test - Sample size: 20 measurements per benchmark - Platform: Modern x86_64 Linux system | Workload Type | Mean Time | Description | Key Achievement | |---------------|-----------|-------------|-----------------| | **UltraFastControl** | **2.387s** | High-frequency control loops | Optimized for high-frequency control | | **FastSensor** | **2.382s** | Rapid sensor processing | Maintains sub-μs sensor fusion | | **HeavyIO** | **3.988s** | I/O-intensive operations | Async tier prevents blocking | | **MixedRealistic** | **4.064s** | Real-world mixed workload | Balanced optimization across tiers | | **FaultTolerance** | **25.485s** | With simulated failures | Failure policy recovery working | ### Scalability Performance The scheduler demonstrates excellent linear scaling: | Node Count | Execution Time | Scaling Factor | |------------|---------------|----------------| | 10 nodes | **106.93ms** | Baseline | | 50 nodes | **113.93ms** | 1.07x (5x nodes) | | 100 nodes | **116.49ms** | 1.09x (10x nodes) | | 200 nodes | **119.55ms** | 1.12x (20x nodes) | **Key Insights:** - Near-linear scaling from 10 to 200 nodes - Only 13ms increase for 20x more nodes - Maintains sub-120ms for large systems - Automatic tier classification optimizes execution order --- ## Real-Time Performance ### RtNode Support HORUS now provides industrial-grade real-time support for safety-critical applications: **RT Features:** - **WCET Enforcement**: Worst-Case Execution Time monitoring - **Deadline Tracking**: Count and handle deadline misses - **Safety Monitor**: Emergency stop on critical failures - **Watchdog Timers**: Detect hung or crashed nodes ### RT Performance Characteristics | Metric | Performance | Description | |--------|-------------|-------------| | **WCET Overhead** | **<5μs** | Cost of monitoring execution time | | **Deadline Precision** | **±10μs** | Jitter in deadline detection | | **Watchdog Resolution** | **1ms** | Minimum detection time | | **Emergency Stop** | **<100μs** | Time to halt all nodes | | **Context Switch** | **<1μs** | Priority preemption overhead | ### Safety-Critical Configuration Running with full safety monitoring enabled: ```rust let scheduler = Scheduler::new().tick_rate(1000_u64.hz()); ``` | Feature | Overhead | Impact | |---------|----------|--------| | WCET Tracking | ~1μs per node | Negligible for >100μs tasks | | Deadline Monitor | ~500ns per node | Sub-microsecond overhead | | Watchdog Feed | ~100ns per tick | Minimal impact | | Safety Checks | ~2μs total | Worth it for safety | | Memory Locking | One-time 10ms | Prevents page faults | ### Real-Time Test Results **Test: Mixed RT and Normal Nodes** - 2 critical RT nodes @ 1kHz - 2 normal nodes @ 100Hz - 2 background nodes @ 10Hz | Node Type | Target Rate | Achieved | Jitter | Misses | |-----------|------------|----------|--------|--------| | RT Critical | 1000 Hz | 999.8 Hz | ±10μs | 0 | | RT High | 500 Hz | 499.9 Hz | ±15μs | 0 | | Normal | 100 Hz | 99.9 Hz | ±50μs | <0.1% | | Background | 10 Hz | 10 Hz | ±200μs | <0.5% | **Zero deadline misses** for critical RT nodes over 1M iterations. --- ## All-Routes Latency HORUS automatically selects the optimal communication path based on topology (same-thread, cross-thread, cross-process) and producer/consumer count. This benchmark measures the latency of each automatically-selected route. ### Benchmark Results | Scenario | Latency | Target | Notes | |----------|---------|--------|-------| | **Same thread, 1:1** | **16ns** | 60ns | Ultra-fast direct path | | **Cross-thread, 1:1** | **11ns** | 60ns | Optimized single-producer path | | **Cross-process, 1:1** | **182ns** | 100ns | Shared memory path | | **Cross-process, N:1** | **244ns** | 150ns | Multi-producer shared memory | | **Cross-process, N:N** | **187ns** | 200ns | General cross-process | ### Latency by Topology | Topology | Producers | Consumers | Latency | |----------|-----------|-----------|---------| | Same thread | 1 | 1 | ~16ns | | Same process | 1 | 1 | ~11ns | | Same process | N | 1 | ~15ns | | Same process | 1 | N | ~15ns | | Same process | N | N | ~20ns | | Cross process | 1 | 1 | ~180ns | | Cross process | N | 1 | ~250ns | | Cross process | 1 | N | ~200ns | | Cross process | N | N | ~190ns | ### Key Achievements - **Sub-20ns** for same-process communication - **Sub-200ns** for cross-process 1:1 - **Sub-300ns** for multi-producer cross-process - **Zero configuration** — optimal path selected automatically - **Seamless migration** — path upgrades transparently as topology changes ### Running the Benchmark ```bash cd horus cargo build --release -p horus_benchmarks ./target/release/all_paths_latency ``` --- ## Summary **HORUS provides production-grade performance for real robotics applications:** **Automatic Path Selection (Recommended):** - **16 ns** — Same-thread - **11 ns** — Cross-thread, 1:1 - **182 ns** — Cross-process, 1:1 - **244 ns** — Cross-process, multi-producer - **187 ns** — Cross-process, multi-producer/consumer **Point-to-Point (1:1):** - **87 ns** — Send only (ultra-low latency) - **161 ns** — CmdVel (motor control) - **262 ns** — Send+Recv round-trip - **~400 ns** — IMU (sensor fusion) - **~120 μs** — PointCloud with 10K points **Multi-Producer/Consumer (N:N):** - **~313 ns** — CmdVel (motor control) - **~500 ns** — IMU (sensor fusion) - **~2.2 μs** — LaserScan (2D lidar) - **~1.1 μs** — Odometry (localization) - **~360 μs** — PointCloud with 10K points **Ready for production deployment** in demanding robotics applications requiring real-time performance with complex data types. --- --- ## Python Benchmarks Real measurements from `horus_py/benchmarks/bench_python.py`. Python 3.12, Linux x86_64. ### Message Send/Recv Latency Single-process Topic roundtrip (send + recv): | Message type | Median | Path | |---|---|---| | `CmdVel` (typed) | **1.5μs** | Zero-copy Pod memcpy | | `Pose2D` (typed) | **1.6μs** | Zero-copy Pod memcpy | | `Imu` (typed) | **1.6μs** | Zero-copy Pod memcpy | | dict `{"v": 1.0}` | **5.4μs** | GenericMessage + MessagePack | | dict `{"x", "y", "z"}` | **9.1μs** | GenericMessage + MessagePack | | dict ~1KB | **52μs** | GenericMessage + MessagePack | Typed messages are **6x faster** than dicts because they skip serialization entirely. ### Zero-Copy Image/PointCloud `to_numpy()` returns a view into shared memory — **constant time regardless of data size**: | Data | `to_numpy()` (zero-copy) | `np.copy()` (naive) | Speedup | |---|---|---|---| | Image 320×240 (225KB) | **3.0μs** | 3.0μs | 1x | | Image 640×480 (900KB) | **3.0μs** | 13μs | **4x** | | Image 1280×720 (2.7MB) | **3.0μs** | 75μs | **25x** | | Image 1920×1080 (6MB) | **3.0μs** | 178μs | **59x** | | PointCloud 10K pts (120KB) | **2.8μs** | — | — | | PointCloud 100K pts (1.2MB) | **2.8μs** | — | — | | DepthImage 640×480 (1.2MB) | **2.8μs** | — | — | | `np.from_dlpack()` (DLPack) | **979ns** | — | — | The key insight: **3μs for a 6MB 1080p image vs 178μs to copy it.** This is the DLPack/shared memory pool advantage — Python gets a pointer to the data, not a copy. ### Node Tick Overhead How fast can the Rust scheduler drive Python nodes: | Scenario | Throughput | Per-tick | |---|---|---| | Empty tick (Rust → Python → Rust) | **~530 Hz** | 1.9ms | | Tick + send(dict) | **~525 Hz** | 1.9ms | | Tick + send(dict) + recv(dict) | **~525 Hz** | 1.9ms | The bottleneck is Python's GIL (~1.8ms per acquisition), not the Rust binding (~30μs). The Rust scheduler, IPC, and safety monitoring add negligible overhead. ### Generic Message Sizes MessagePack serialization for common robotics data: | Payload | Bytes | Fits in GenericMessage? | |---|---|---| | Empty dict | 1 | Yes (4KB max) | | CmdVel-like `{linear, angular}` | 34 | Yes | | IMU-like (accel + gyro + mag) | 100 | Yes | | LaserScan 360 rays | 3,251 | Yes | | 10 detections | 374 | Yes | ### Running Python Benchmarks ```bash cd horus_py PYTHONPATH=. python3 benchmarks/bench_python.py ``` --- ## Next Steps - Learn how to maximize performance: [Performance Optimization](/performance/performance) - Explore message types: [Message Types](/concepts/message-types) - See usage examples: [Examples](/rust/examples/basic-examples) - Get started: [Quick Start](/getting-started/quick-start) **Build faster. Debug easier. Deploy with confidence.** ======================================== # SECTION: Operations ======================================== --- ## Deploy to Your Robot Path: /operations/deploy-to-robot Description: Step-by-step guide to deploying HORUS projects from your development PC to a robot # Deploy to Your Robot This guide walks through every step from "my project works on my PC" to "it's running on my robot." No prior SSH or networking experience required. ## How It Works `horus deploy` is a three-step pipeline that runs on your **development PC**: ``` Your PC Your Robot ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ │ │ Step 1: Build │ │ │ │ Cross-compiles your │ │ │ │ code for the robot's│ │ │ │ architecture (ARM) │ │ │ │ │ Step 2: Sync │ │ │ Your compiled │ ─── rsync ───> │ ~/horus_deploy/ │ │ binary + project │ over SSH │ your binary lands │ │ files │ │ here │ │ │ Step 3: Run │ │ │ │ ─── ssh ─────> │ Binary executes │ │ │ │ directly │ └──────────────────────┘ └──────────────────────┘ ``` Your code is compiled on your PC and the finished binary is copied to the robot. The robot just runs it. ## What Each Machine Needs ### Your Development PC | Requirement | Why | How to install | |-------------|-----|----------------| | HORUS CLI | Builds and deploys your project | [Installation guide](/getting-started/installation) | | Rust toolchain | Cross-compiles for robot architecture | Installed with HORUS | | rsync | Syncs files to robot efficiently | `sudo apt install rsync` (usually pre-installed) | | SSH client | Connects to robot | `sudo apt install openssh-client` (usually pre-installed) | **Supported dev PC operating systems:** Linux, macOS, WSL 2. Native Windows without WSL is not supported (no rsync/SSH). ### Your Robot | Requirement | Why | |-------------|-----| | Linux | Any distribution (Raspberry Pi OS, Ubuntu, Debian, etc.) | | SSH server enabled | So your PC can connect and copy files | | Network connection | WiFi or Ethernet, same network as your PC | **The robot does NOT need:** HORUS, Rust, cargo, or any build tools. The compiled binary is self-contained. **Exception for Python projects:** The robot also needs `python3` and the HORUS Python package: ```bash # Run this ON the robot (one-time setup) pip install horus-robotics ``` ## Step-by-Step ### Step 1: Prepare Your Robot If you have a new Raspberry Pi or Jetson that isn't set up yet: **Raspberry Pi:** 1. Download [Raspberry Pi Imager](https://www.raspberrypi.com/software/) on your PC 2. Flash an SD card with **Raspberry Pi OS** (or Ubuntu Server) 3. In the imager settings (gear icon), configure: - **Username and password** (e.g., `pi` / `yourpassword`) - **WiFi** network name and password - **Enable SSH** (check the box) 4. Insert the SD card into the Pi and power it on 5. Wait 1-2 minutes for it to boot and connect to WiFi **Jetson Nano/Xavier/Orin:** 1. Flash the SD card or eMMC with NVIDIA JetPack (Ubuntu-based) 2. Complete the first-boot setup (username, password, WiFi) 3. SSH is enabled by default on JetPack **Any other Linux board:** - Ensure SSH is enabled: `sudo systemctl enable ssh && sudo systemctl start ssh` - Ensure it's connected to the same network as your PC ### Step 2: Find Your Robot's IP Address Your PC and robot must be on the same network (same WiFi or same Ethernet switch). You need the robot's IP address to connect. **Option A: mDNS (easiest, try this first)** Most Linux boards advertise their hostname on the local network: ```bash # On your PC: ping raspberrypi.local # Raspberry Pi default hostname ping jetson.local # Jetson default hostname ``` If it responds, you can use `raspberrypi.local` instead of an IP address: ```bash horus deploy pi@raspberrypi.local --run ``` **Option B: Check your router** 1. Open your router's admin page in a browser (usually `192.168.1.1` or `192.168.0.1`) 2. Look for "Connected Devices" or "DHCP Clients" 3. Find your robot's hostname (e.g., "raspberrypi") and note its IP **Option C: Scan your network** ```bash # On your PC — scan for all devices: nmap -sn 192.168.1.0/24 # Or if your network is 192.168.0.x: nmap -sn 192.168.0.0/24 ``` Look for your robot's hostname or MAC address in the results. **Option D: Connect a monitor to the robot** Plug a monitor and keyboard into the robot and run: ```bash hostname -I # Output: 192.168.1.50 ``` ### Step 3: Test SSH Connection Before deploying, verify you can connect to the robot: ```bash ssh pi@192.168.1.50 # Enter your password when prompted ``` Replace `pi` with your robot's username and `192.168.1.50` with the IP you found. If you see the robot's terminal prompt, it works. Type `exit` to disconnect. **If "Connection refused":** SSH is not enabled on the robot. Connect a monitor and keyboard to the robot and run: ```bash sudo systemctl enable ssh sudo systemctl start ssh ``` ### Step 4: Set Up SSH Keys (Recommended) Without SSH keys, `horus deploy` will prompt for your password during every deployment. Setting up keys makes it passwordless: ```bash # On your PC — generate a key (press Enter for all prompts): ssh-keygen -t ed25519 # Copy it to the robot: ssh-copy-id pi@192.168.1.50 # Enter your password one last time # Verify — this should connect WITHOUT asking for a password: ssh pi@192.168.1.50 ``` ### Step 5: Deploy In your project directory on your PC: ```bash horus deploy pi@192.168.1.50 --run ``` You'll see: ``` HORUS Deploy Target: pi@192.168.1.50 Remote dir: ~/horus_deploy Architecture: ARM64 (aarch64) Build mode: release Run after: true Step 1: Building project... Checking target aarch64-unknown-linux-gnu... OK Building for ARM64 (aarch64)... Build complete Step 2: Syncing files to target... Will sync '/home/you/my-robot' to pi@192.168.1.50:~/horus_deploy/ (with --delete) Files on remote not present locally will be DELETED Continue? [y/N] y Syncing files... Files synced Step 3: Running on target... Running: ./target/aarch64-unknown-linux-gnu/release/my_robot Press Ctrl+C to stop [SCHEDULER] Started with 3 nodes at 100 Hz [motor_controller] Initialized ... ``` Press `Ctrl+C` to stop the robot. ### Step 6: Save Your Target Typing the full `pi@192.168.1.50` every time gets tedious. Create a `deploy.yaml` file in your project root: ```yaml targets: robot: host: pi@192.168.1.50 arch: aarch64 ``` Now deploy with just a name: ```bash horus deploy robot --run ``` For multiple robots, add more targets: ```yaml targets: robot: host: pi@192.168.1.50 arch: aarch64 jetson: host: nvidia@jetson.local arch: aarch64 port: 2222 identity: ~/.ssh/jetson_key arm-pc: host: ubuntu@10.0.0.20 arch: x86_64 dir: ~/my_app ``` ## Deploy Options | Option | What it does | Example | |--------|-------------|---------| | `--run` | Execute the binary after deploying | `horus deploy robot --run` | | `--dry-run` | Show what would happen without doing it | `horus deploy robot --dry-run` | | `--arch` | Override target architecture | `horus deploy robot --arch armv7` | | `--debug` | Build in debug mode (faster build, slower binary) | `horus deploy robot --debug` | | `-d, --dir` | Custom remote directory (default: `~/horus_deploy`) | `horus deploy robot -d /opt/robot` | | `-p, --port` | Custom SSH port (default: 22) | `horus deploy robot -p 2222` | | `-i, --identity` | SSH private key file | `horus deploy robot -i ~/.ssh/robot_key` | | `--all` | Deploy to every target in deploy.yaml | `horus deploy --all --run` | | `--list` | Show configured targets | `horus deploy --list` | ## Fleet Deployment Deploy to all robots with one command: ```bash horus deploy --all --run ``` This builds **once** and syncs to each robot. If one robot is unreachable, the others still get deployed: ``` HORUS Fleet Deploy (3 targets) 1. robot -> pi@192.168.1.50 2. jetson -> nvidia@jetson.local 3. arm-pc -> ubuntu@10.0.0.20 Step 1: Building for ARM64 (shared across 3 targets)... Build complete Step 2: Syncing to 3 targets... This will sync to 3 remote hosts (with --delete) Continue? [y/N] y --- [1/3] robot Files synced [1/3] robot done --- [2/3] jetson [2/3] jetson done --- [3/3] arm-pc [x] arm-pc failed: SSH connection refused === Fleet Deploy Summary 2/3 targets deployed successfully ``` ## What Gets Copied to the Robot The project directory is synced to `~/horus_deploy/` on the robot, **excluding**: - `target/` (build artifacts — only the final binary is kept) - `.git/` (version control history) - `__pycache__/` and `*.pyc` (Python cache) - `node_modules/` (JavaScript dependencies) The rsync `--delete` flag keeps the remote directory in sync — files you delete locally are also deleted on the robot. ## Architecture Auto-Detection If you don't specify `--arch`, horus guesses from the hostname: | Hostname contains | Detected architecture | |---|---| | `jetson`, `nano`, `xavier`, `orin` | aarch64 | | `pi4`, `pi5`, `raspberry` | aarch64 | | `pi3`, `pi2` | armv7 | | Anything else | aarch64 (default) | Override with `--arch` if the auto-detection is wrong: ```bash horus deploy myrobot@10.0.0.5 --arch x86_64 ``` ## Rust vs Python Differences | Aspect | Rust project | Python project | |--------|-------------|---------------| | Build step | Cross-compiles for target architecture | Skipped (no compilation needed) | | Robot requirements | Nothing beyond Linux + SSH | Python 3 + `pip install horus-robotics` | | What runs on robot | Single binary (`./target/.../my_robot`) | `python3 src/main.py` | | Binary size | Self-contained (~5-50 MB) | Source files only | | First deploy speed | Slower (compilation) | Faster (just file sync) | ## Troubleshooting ### "Connection refused" when deploying SSH is not enabled or not running on the robot: ```bash # Connect a monitor/keyboard to the robot and run: sudo systemctl enable ssh sudo systemctl start ssh ``` ### "Permission denied (publickey)" The robot is rejecting your SSH key or password: ```bash # Try with explicit password authentication: ssh -o PreferredAuthentications=password pi@192.168.1.50 # If that works, re-copy your key: ssh-copy-id pi@192.168.1.50 ``` ### "rsync: command not found" Install rsync on your **development PC**: ```bash # Ubuntu/Debian: sudo apt install rsync # macOS: brew install rsync ``` ### Binary crashes or "Exec format error" on robot Wrong architecture. The binary was compiled for a different CPU than the robot has: ```bash # Check what the robot actually runs: ssh pi@192.168.1.50 "uname -m" # aarch64 = use --arch aarch64 # armv7l = use --arch armv7 # x86_64 = use --arch x86_64 # Re-deploy with the correct architecture: horus deploy pi@192.168.1.50 --arch armv7 --run ``` ### "cargo build failed" during cross-compilation The cross-compilation target may not be installed. Horus installs it automatically, but if it fails: ```bash # Install manually on your PC: rustup target add aarch64-unknown-linux-gnu # For ARM 32-bit: rustup target add armv7-unknown-linux-gnueabihf ``` You may also need the cross-compilation linker: ```bash # Ubuntu/Debian: sudo apt install gcc-aarch64-linux-gnu # For ARM 32-bit: sudo apt install gcc-arm-linux-gnueabihf ``` ### "ping raspberrypi.local" doesn't work mDNS may not be available. Try: ```bash # Install mDNS support on your PC: sudo apt install avahi-utils # Install on the robot (connect monitor/keyboard): sudo apt install avahi-daemon ``` Or skip mDNS and find the IP through your router or `nmap` instead. ### Deploy works but program doesn't find hardware (GPIO, serial, I2C) The robot user may not have permission to access hardware devices: ```bash # On the robot: sudo usermod -aG dialout,gpio,i2c,spi pi # Log out and back in for changes to take effect ``` ## See Also - [Operations Overview](/operations) — Quick reference for all operations commands - [Production Deployment](/advanced/deployment) — RT kernel, safety monitoring, and performance tuning - [CLI Reference](/development/cli-reference) — Full `horus deploy` flag reference --- ## Operations Path: /operations Description: Deploy, monitor, and maintain HORUS robotics applications in production # Operations From development to production deployment, fleet management, and ongoing maintenance. --- ## Quick Reference | Task | Command | |------|---------| | Deploy to one robot | `horus deploy pi@192.168.1.100` | | Deploy to named target | `horus deploy jetson-01` | | Deploy to all robots | `horus deploy --all` | | List deploy targets | `horus deploy --list` | | Monitor running system | `horus monitor` | | Check system health | `horus doctor` | | View flight recorder | `horus blackbox` | | Record a session | `horus run --record session1` | | Replay a recording | `horus record replay session1` | --- ## Deployment New to deploying? See **[Deploy to Your Robot](/operations/deploy-to-robot)** for the full setup guide — from preparing your robot to running your first deploy. ### Single Robot Deploy directly to a host: ```bash # Build, sync, and deploy horus deploy pi@192.168.1.100 # Deploy and run immediately horus deploy pi@192.168.1.100 --run # Deploy to specific architecture horus deploy ubuntu@jetson.local --arch aarch64 # Preview without deploying horus deploy pi@192.168.1.100 --dry-run ``` ### Named Targets Configure robots in `deploy.yaml` (project root): ```yaml targets: jetson-01: host: nvidia@10.0.0.1 arch: aarch64 dir: ~/robot jetson-02: host: nvidia@10.0.0.2 arch: aarch64 dir: ~/robot arm-controller: host: pi@10.0.0.10 arch: aarch64 dir: ~/arm port: 2222 identity: ~/.ssh/robot_key ``` Deploy by name: ```bash horus deploy jetson-01 horus deploy jetson-01 --run ``` ### Fleet Deployment Deploy to multiple robots at once: ```bash # Multiple named targets horus deploy jetson-01 jetson-02 jetson-03 # All targets from deploy.yaml horus deploy --all # Preview fleet deployment horus deploy --all --dry-run # List all configured targets horus deploy --list ``` Fleet deploy builds **once** (shared binary for same architecture), then syncs to each robot sequentially. Confirmation is asked once for the entire fleet, not per robot. If a deployment fails for one robot, the fleet continues to the next. A summary is printed at the end: ``` HORUS Fleet Deploy (3 targets) --- [1/3] jetson-01 [checkmark] jetson-01 done --- [2/3] jetson-02 [checkmark] jetson-02 done --- [3/3] jetson-03 [x] jetson-03 failed: SSH connection refused === Fleet Deploy Summary [checkmark] 2/3 targets deployed successfully ``` ### Supported Architectures | Architecture | Alias | Common Robots | |-------------|-------|---------------| | `aarch64` | `arm64`, `jetson`, `pi4`, `pi5` | Raspberry Pi 4/5, Jetson Nano/Xavier/Orin | | `armv7` | `arm`, `pi3`, `pi2` | Raspberry Pi 2/3, older ARM boards | | `x86_64` | `x64`, `amd64`, `intel` | Intel NUC, standard PCs | | `native` | `host`, `local` | Same as build machine | --- ## Monitoring ### Web Dashboard ```bash horus monitor # Opens web dashboard at http://localhost:4200 ``` The monitor shows: - **Active nodes** with health status, tick rates, CPU/memory usage - **Topic graph** with message flow and rates - **Parameters** with live editing - **Packages** with install/uninstall - **API Docs** with searchable symbol browser and topic flow visualization - **Logs** with filtering by node and severity ### TUI Dashboard ```bash horus monitor --tui # Terminal-based dashboard (no browser needed) ``` ### Programmatic Access ```bash # Node status horus node list --json # Topic rates horus topic list --json # System health horus doctor --json ``` --- ## Environment Management ### Reproducible Builds `horus.lock` pins every dependency to an exact version. Commit it to git, and `horus build` on another machine installs identical deps. --- ## Flight Recorder (BlackBox) The BlackBox records the last N events before a crash — like an airplane's black box. ```bash # Enable in code let scheduler = Scheduler::new() .with_blackbox(64) // Keep last 64MB .tick_rate(100_u64.hz()); # View after crash horus blackbox horus blackbox --json horus blackbox anomalies ``` --- ## Record and Replay Record a session for debugging or regression testing: ```bash # Record horus run --record session1 # List recordings horus record list # Replay horus record replay session1 # Compare two recordings horus record diff session1 session2 # Export for analysis horus record export session1 --format mcap ``` --- ## Health Checks ```bash # Full system health check horus doctor # JSON output for CI horus doctor --json ``` The doctor checks: - Rust toolchain (cargo, rustc, clippy, fmt) - Python toolchain (python3, ruff, pytest) - Project manifest validity - Shared memory status - Disk usage - Plugin registry connectivity --- ## Deployment Checklist Before deploying to production: 1. **Enable safety monitoring** — `.watchdog(500_u64.ms())` 2. **Configure real-time** — `.prefer_rt()` for graceful RT, `.require_rt()` for strict 3. **Run tests** — `horus test` 4. **Check health** — `horus doctor` 5. **Deploy** — `horus deploy --run` 7. **Monitor** — `horus monitor` from your development machine See [Production Deployment](/advanced/deployment) for the full guide. --- ## See Also - [Production Deployment](/advanced/deployment) — RT setup, safety, performance tuning - [Scheduler Configuration](/advanced/scheduler-configuration) — Tick rates, budgets, deadlines - [Package Management](/package-management/package-management) — Installing and managing packages - [CLI Reference](/development/cli-reference) — All 46+ commands ======================================== # SECTION: Reference ======================================== --- ## API Quick Reference Path: /reference/api-index Description: Flat index of every public type, function, and CLI command — for fast lookup by humans and AI # API Quick Reference One-page index of every HORUS API. Use Ctrl+F to find what you need. ## Message Types — "What type do I use for...?" ### Motion & Control | Task | Type | Constructor | Topic | |------|------|-------------|-------| | Move a wheeled robot | `CmdVel` | `CmdVel::new(linear, angular)` | `cmd_vel` | | Move a differential drive | `DifferentialDriveCommand` | `DifferentialDriveCommand::new(left, right)` | `drive_cmd` | | Control a servo | `ServoCommand` | `ServoCommand::new(position, velocity)` | `servo_cmd` | | Control a joint | `JointCommand` | `JointCommand::for_joint(id, position)` | `joint_cmd` | | Control a motor | `MotorCommand` | `MotorCommand::velocity(rpm)` | `motor_cmd` | | Follow a trajectory | `TrajectoryPoint` | `TrajectoryPoint::new(position, velocity)` | `trajectory` | | PID tuning | `PidConfig` | `PidConfig::new(kp, ki, kd)` | `pid_config` | ### Sensors | Task | Type | Constructor | Topic | |------|------|-------------|-------| | LiDAR scan | `LaserScan` | `LaserScan::from_ranges(&ranges)` | `scan` | | Camera image | `Image` | `Image::rgb(w, h, &pixels)` | `image` | | Depth image | `DepthImage` | `DepthImage::new(w, h, &depths)` | `depth` | | Compressed image | `CompressedImage` | `CompressedImage::jpeg(&bytes)` | `image/compressed` | | IMU (accel+gyro) | `Imu` | `Imu::new(ax, ay, az, gx, gy, gz)` | `imu` | | Odometry (pose+velocity) | `Odometry` | `Odometry::new(x, y, theta)` | `odom` | | Joint positions | `JointState` | `JointState::from_positions(&pos)` | `joint_states` | | Battery level | `BatteryState` | `BatteryState::new(voltage, percentage)` | `battery` | | Temperature | `Temperature` | `Temperature::celsius(value)` | `temperature` | | GPS | `NavSatFix` | `NavSatFix::new(lat, lon, alt)` | `gps` | | Range sensor | `RangeSensor` | `RangeSensor::new(distance)` | `range` | | Magnetic field | `MagneticField` | `MagneticField::new(x, y, z)` | `mag` | | Audio | `AudioFrame` | `AudioFrame::mono(sample_rate, &samples)` | `audio` | ### Perception | Task | Type | Constructor | |------|------|-------------| | 2D object detection | `Detection` | `Detection::new(label, confidence, bbox)` | | 3D object detection | `Detection3D` | `Detection3D::new(label, confidence, bbox3d)` | | 2D bounding box | `BoundingBox2D` | `BoundingBox2D::new(x, y, w, h)` | | 3D bounding box | `BoundingBox3D` | `BoundingBox3D::new(center, size)` | | Object tracking | `TrackedObject` | `TrackedObject::new(id, detection)` | | Segmentation mask | `SegmentationMask` | `SegmentationMask::new(w, h, &classes)` | | Point cloud | `PointCloud` | `PointCloud::from_xyz(&points)` | | Plane detection | `PlaneDetection` | `PlaneDetection::new(normal, distance)` | | Landmark | `Landmark` | `Landmark::new(x, y, confidence)` | ### Navigation | Task | Type | Constructor | |------|------|-------------| | Send a goal | `NavGoal` | `NavGoal::new(x, y, theta)` | | Plan a path | `PathPlan` | `PathPlan::from_waypoints(&waypoints)` | | Occupancy grid | `OccupancyGrid` | `OccupancyGrid::new(w, h, resolution)` | | Cost map | `CostMap` | `CostMap::new(w, h, resolution)` | | Waypoint | `Waypoint` | `Waypoint::new(x, y)` | ### Geometry | Task | Type | Constructor | |------|------|-------------| | 3D point | `Point3` | `Point3::new(x, y, z)` | | 3D vector | `Vector3` | `Vector3::new(x, y, z)` | | Rotation | `Quaternion` | `Quaternion::identity()` | | 2D pose | `Pose2D` | `Pose2D::new(x, y, theta)` | | 3D pose | `Pose3D` | `Pose3D::new(position, orientation)` | | Velocity | `Twist` | `Twist::new(linear, angular)` | | Acceleration | `Accel` | `Accel::new(linear, angular)` | | Transform | `TransformStamped` | `TransformStamped::new(parent, child, translation, rotation)` | ### Force & Contact | Task | Type | Constructor | |------|------|-------------| | Force command | `ForceCommand` | `ForceCommand::force_only(force_vec)` | | Impedance control | `ImpedanceParameters` | `ImpedanceParameters::new(stiffness, damping)` | | Wrench (force+torque) | `WrenchStamped` | `WrenchStamped::new(force, torque)` | | Contact info | `ContactInfo` | `ContactInfo::new(force, position)` | ### Diagnostics | Task | Type | Constructor | |------|------|-------------| | Status report | `DiagnosticStatus` | `DiagnosticStatus::ok("message")` | | Emergency stop | `EmergencyStop` | `EmergencyStop::trigger("reason")` | | Heartbeat | `Heartbeat` | `Heartbeat::now()` | | Resource usage | `ResourceUsage` | `ResourceUsage::new(cpu, memory)` | --- ## Core API Cheatsheet ```rust use horus::prelude::*; // ── Create a scheduler ──────────────────────────── let mut scheduler = Scheduler::new() .tick_rate(100.hz()) // 100 Hz main loop .monitoring(true) // Enable health monitoring .deterministic(true); // Simulation mode (optional) // ── Add a node ──────────────────────────────────── scheduler.add(MyNode::new()) .order(0) // Execution priority .rate(50.hz()) // Node tick rate .budget(2.ms()) // Max tick duration .deadline(5.ms()) // Hard deadline .on_miss(Miss::Skip) // What to do on deadline miss .build()?; // ── Run ─────────────────────────────────────────── scheduler.run()?; // Blocks until Ctrl+C // ── Topics (pub/sub) ────────────────────────────── let pub_topic: Topic = Topic::new("cmd_vel")?; pub_topic.send(CmdVel::new(1.0, 0.0)); let sub_topic: Topic = Topic::new("scan")?; if let Some(scan) = sub_topic.try_recv() { println!("Got {} ranges", scan.ranges.len()); } // ── Time ────────────────────────────────────────── let now = horus::now(); // Current ClockInstant let dt = horus::dt(); // Time since last tick let elapsed = horus::elapsed(); // Total runtime // ── Parameters ──────────────────────────────────── let params = RuntimeParams::new(); params.set("speed", 1.5)?; let speed: f64 = params.get_or("speed", 1.0); // ── Duration/Frequency helpers ──────────────────── let rate = 100_u64.hz(); // 100 Hz let budget = 200_u64.us(); // 200 microseconds let deadline = 1_u64.ms(); // 1 millisecond ``` --- ## CLI Commands | Command | Purpose | |---------|---------| | `horus new NAME -r/-p` | Create Rust/Python project | | `horus run` | Build and run | | `horus build` | Build only | | `horus test` | Run tests | | `horus add NAME --source SOURCE` | Add dependency | | `horus remove NAME` | Remove dependency | | `horus install PKG` | Install package from registry | | `horus topic list` | List active topics | | `horus node list` | List running nodes | | `horus param get/set KEY VALUE` | Runtime parameters | | `horus monitor` | Web dashboard | | `horus monitor --tui` | Terminal dashboard | | `horus log` | View logs | | `horus doctor` | Health check | | `horus check` | Validate project | | `horus fmt` | Format code | | `horus lint` | Lint code | | `horus deploy TARGET` | Deploy to robot | | `horus publish` | Publish to registry | | `horus auth login` | Authenticate | ## See Also - [Getting Started](/learn/quick-start) — First project in 5 minutes - [Tutorials](/tutorials) — Step-by-step guided examples - [Rust API](/rust/api/scheduler) — Full Rust API reference - [Python API](/python/api/python-bindings) — Python bindings reference - [CLI Reference](/development/cli-reference) — Complete CLI documentation --- ## API Cheatsheet Path: /reference/api-cheatsheet Description: Every public type signature in horus — Node, Topic, Scheduler, Messages, Services, Actions, TransformFrame — in one page. ## Node Trait | Method | Signature | Default | Description | |--------|-----------|---------|-------------| | `name` | `fn name(&self) -> &str` | Type name | Unique node identifier | | `init` | `fn init(&mut self) -> Result<()>` | `Ok(())` | Called once at startup | | `tick` | `fn tick(&mut self)` | *required* | Main loop, called repeatedly | | `shutdown` | `fn shutdown(&mut self) -> Result<()>` | `Ok(())` | Called once at cleanup | | `publishers` | `fn publishers(&self) -> Vec` | `vec![]` | Topic metadata for pubs | | `subscribers` | `fn subscribers(&self) -> Vec` | `vec![]` | Topic metadata for subs | | `on_error` | `fn on_error(&mut self, error: &str)` | Logs via `hlog!` | Custom error recovery | | `is_safe_state` | `fn is_safe_state(&self) -> bool` | `true` | Safety monitor query | | `enter_safe_state` | `fn enter_safe_state(&mut self)` | No-op | Emergency stop transition | ## NodeBuilder | Method | Parameter | Description | |--------|-----------|-------------| | `order` | `u32` | Execution priority (lower = earlier). 0-9 critical, 10-49 high, 50-99 normal, 100+ low | | `rate` | `Frequency` | Tick rate. Auto-derives budget (80%) and deadline (95%). Auto-enables RT for BestEffort nodes | | `budget` | `Duration` | Max tick execution time. Overrides auto-derived 80% budget | | `deadline` | `Duration` | Absolute latest tick finish. Overrides auto-derived 95% deadline | | `on_miss` | `Miss` | Deadline miss policy: `Warn`, `Skip`, `SafeMode`, `Stop` | | `compute` | — | Parallel thread pool execution (CPU-bound work) | | `on` | `&str` | Event-triggered on topic update | | `async_io` | — | Tokio blocking pool execution (I/O-bound work) | | `failure_policy` | `FailurePolicy` | Per-node failure handling: `fatal()`, `restart(n, backoff)`, `skip()`, `ignore()` | | `priority` | `i32` | OS-level SCHED_FIFO priority (1-99). RT nodes only | | `core` | `usize` | Pin RT thread to CPU core via `sched_setaffinity` | | `watchdog` | `Duration` | Per-node watchdog timeout (overrides global) | | `build` | — | Finalize and register node. Returns `Result<&mut Scheduler>` | ## Scheduler | Method | Parameter | Description | |--------|-----------|-------------| | `Scheduler::new()` | — | Constructor with RT capability auto-detection | | `Scheduler::simulation()` | — | Constructor that skips RT detection (for tests) | | `name` | `&str` | Set scheduler name (default: `"Scheduler"`) | | `tick_rate` | `Frequency` | Global tick rate (e.g. `1000_u64.hz()`) | | `deterministic` | `bool` | Sequential execution, SimClock, fixed dt, seeded RNG | | `prefer_rt` | — | Try mlockall + SCHED_FIFO, degrade gracefully | | `require_rt` | — | Panic if RT unavailable | | `cores` | `&[usize]` | Pin scheduler threads to CPU cores | | `watchdog` | `Duration` | Frozen-node detection timeout | | `blackbox` | `usize` | Flight recorder size in MB | | `max_deadline_misses` | `u64` | Emergency stop threshold (default: 100) | | `verbose` | `bool` | Enable/disable executor thread logging | | `with_recording` | — | Enable session recording | | `telemetry` | `&str` | Export endpoint (`"udp://host:port"`) | | `add` | `impl Node` | Returns `NodeBuilder` for fluent configuration | | `set_node_rate` | `(&str, Frequency)` | Change node rate at runtime | | `run` | — | Start the main loop (blocks) | | `run_for` | `Duration` | Run for a fixed duration | | `run_ticks` | `u64` | Run exactly N ticks | | `run_until` | `FnMut() -> bool, u64` | Run until predicate or max ticks | | `tick_once` | — | Single-tick execution (sim/test). Lazy-inits on first call | | `stop` | — | Signal scheduler to stop | | `status` | — | Human-readable status report | ## `Topic` | Method | Signature | Description | |--------|-----------|-------------| | `new` | `fn new(name: impl Into) -> Result` | Create topic with auto-detected backend | | `send` | `fn send(&self, msg: T)` | Fire-and-forget with bounded retry | | `try_send` | `fn try_send(&self, msg: T) -> Result<(), T>` | Non-blocking send, returns msg on failure | | `send_blocking` | `fn send_blocking(&self, msg: T, timeout: Duration) -> Result<(), SendBlockingError>` | Blocking send with spin-yield-sleep strategy | | `recv` | `fn recv(&self) -> Option` | Receive next message | | `try_recv` | `fn try_recv(&self) -> Option` | Non-blocking receive (no logging) | | `read_latest` | `fn read_latest(&self) -> Option where T: Copy` | Peek latest without advancing consumer | | `name` | `fn name(&self) -> &str` | Topic name | | `metrics` | `fn metrics(&self) -> TopicMetrics` | Send/recv counts and failure counts | ## Services | Type | Method | Description | |------|--------|-------------| | `ServiceClient` | `new() -> Result` | Create blocking client for service `S` | | `ServiceClient` | `call(req, timeout) -> Result` | Blocking RPC call | | `ServiceClient` | `call_with_retry(req, timeout, RetryConfig) -> Result` | Call with retry policy | | `AsyncServiceClient` | `new() -> Result` | Create non-blocking client | | `AsyncServiceClient` | `call_async(req) -> PendingResponse` | Non-blocking call, returns handle | | `ServiceServerBuilder` | `new() -> Self` | Start building a server | | `ServiceServerBuilder` | `on_request(Fn(Req) -> Result) -> Self` | Set request handler | | `ServiceServerBuilder` | `build() -> Result>` | Spawn background polling thread | | `ServiceServer` | `stop(&self)` | Stop server (also happens on drop) | ## Actions | Type | Method | Description | |------|--------|-------------| | `ActionClientBuilder` | `new() -> Self` | Start building an action client | | `ActionClientBuilder` | `build() -> Result>` | Create client node | | `ActionClientNode` | `send_goal(goal) -> ClientGoalHandle` | Send goal, get tracking handle | | `ActionClientNode` | `send_goal_with_priority(goal, GoalPriority) -> ClientGoalHandle` | Send prioritized goal | | `ActionClientNode` | `cancel_goal(GoalId)` | Cancel a running goal | | `ClientGoalHandle` | `status() -> GoalStatus` | Current goal status | | `ClientGoalHandle` | `is_active() -> bool` | Still running? | | `ClientGoalHandle` | `is_done() -> bool` | Terminal state? | | `ClientGoalHandle` | `result() -> Option` | Get result if complete | | `ClientGoalHandle` | `last_feedback() -> Option` | Most recent feedback | | `ClientGoalHandle` | `await_result(timeout) -> Option` | Block until done or timeout | | `ClientGoalHandle` | `await_result_with_feedback(timeout, Fn(&Feedback))` | Block with feedback callback | | `ActionServerBuilder` | `new() -> Self` | Start building an action server | | `ActionServerBuilder` | `on_goal(Fn(Goal) -> GoalResponse) -> Self` | Accept/reject handler | | `ActionServerBuilder` | `on_cancel(Fn(GoalId) -> CancelResponse) -> Self` | Cancel handler | | `ActionServerBuilder` | `on_execute(Fn(ServerGoalHandle) -> GoalOutcome) -> Self` | Execution handler | | `ActionServerBuilder` | `build() -> Result>` | Create server node | | `ServerGoalHandle` | `goal() -> &A::Goal` | Access goal data | | `ServerGoalHandle` | `is_cancel_requested() -> bool` | Client requested cancel? | | `ServerGoalHandle` | `should_abort() -> bool` | Cancel or preempt requested? | | `ServerGoalHandle` | `publish_feedback(feedback)` | Send progress feedback | | `ServerGoalHandle` | `succeed(result) -> GoalOutcome` | Complete successfully | | `ServerGoalHandle` | `abort(result) -> GoalOutcome` | Server-side abort | | `ServerGoalHandle` | `canceled(result) -> GoalOutcome` | Acknowledge cancellation | | `ServerGoalHandle` | `preempted(result) -> GoalOutcome` | Preempted by higher-priority goal | ## TransformFrame | Method | Signature | Description | |--------|-----------|-------------| | `new` | `fn new() -> Self` | Create empty TF tree (default config) | | `register_frame` | `fn register_frame(&self, name: &str, parent: Option<&str>) -> Result` | Add a frame to the tree | | `tf` | `fn tf(&self, src: &str, dst: &str) -> Result` | Lookup latest transform between frames | | `tf_at` | `fn tf_at(&self, src: &str, dst: &str, timestamp_ns: u64) -> Result` | Lookup transform at a specific time | | `tf_at_strict` | `fn tf_at_strict(&self, src: &str, dst: &str, timestamp_ns: u64) -> Result` | Strict time lookup (no interpolation beyond tolerance) | | `tf_at_with_tolerance` | `fn tf_at_with_tolerance(&self, src: &str, dst: &str, ts: u64, tol: u64) -> Result` | Custom time tolerance | | `update_transform` | `fn update_transform(&self, parent: &str, child: &str, ts: u64, tf: Transform) -> Result<()>` | Update a frame's transform | | `has_frame` | `fn has_frame(&self, name: &str) -> bool` | Check if frame exists | | `all_frames` | `fn all_frames(&self) -> Vec` | List all registered frame names | | `frame_count` | `fn frame_count(&self) -> usize` | Number of registered frames | | `tf_by_id` | `fn tf_by_id(&self, src: FrameId, dst: FrameId) -> Option` | Lookup by numeric ID (fast path) | | `tf_at_by_id` | `fn tf_at_by_id(&self, src: FrameId, dst: FrameId, ts: u64) -> Option` | Time-based lookup by ID | ## DurationExt | Method | Input | Output | Example | |--------|-------|--------|---------| | `ns` | `u64`, `f64`, `i32` | `Duration` | `500_u64.ns()` | | `us` | `u64`, `f64`, `i32` | `Duration` | `200_u64.us()` | | `ms` | `u64`, `f64`, `i32` | `Duration` | `1_u64.ms()` | | `secs` | `u64`, `f64`, `i32` | `Duration` | `5_u64.secs()` | | `hz` | `u64`, `f64`, `i32` | `Frequency` | `100_u64.hz()` | `Frequency` methods: `value() -> f64`, `period() -> Duration`, `budget_default() -> Duration` (80%), `deadline_default() -> Duration` (95%). ## Enums ### ExecutionClass | Variant | Description | |---------|-------------| | `Rt` | Dedicated RT thread with spin-wait timing | | `Compute` | Parallel thread pool for CPU-bound work | | `Event(String)` | Triggered by topic updates | | `AsyncIo` | Tokio blocking pool for I/O-bound work | | `BestEffort` | Default -- main tick loop, sequential | ### Miss | Variant | Description | |---------|-------------| | `Warn` | Log warning and continue (default) | | `Skip` | Skip this tick, resume next cycle | | `SafeMode` | Call `enter_safe_state()`, continue ticking in safe mode | | `Stop` | Stop the entire scheduler | ### NodeState | Variant | Description | |---------|-------------| | `Uninitialized` | Created but not started | | `Initializing` | `init()` in progress | | `Running` | Normal operation | | `Stopping` | `shutdown()` in progress | | `Stopped` | Cleanly stopped | | `Error(String)` | Error occurred, still running | | `Crashed(String)` | Fatal error, unresponsive | ### HealthStatus | Variant | Description | |---------|-------------| | `Healthy` | Operating normally | | `Warning` | Degraded performance (slow ticks, missed deadlines) | | `Error` | Errors occurring but still running | | `Critical` | Fatal errors, about to crash | | `Unknown` | No heartbeat received (default) | ## Standard Message Types ### Geometry | Type | Fields | Size | |------|--------|------| | `Pose2D` | `x: f64, y: f64, theta: f64` | 24 B | | `Pose3D` | `translation: [f64; 3], rotation: [f64; 4]` | 56 B | | `Point3` | `x: f64, y: f64, z: f64` | 24 B | | `Vector3` | `x: f64, y: f64, z: f64` | 24 B | | `Quaternion` | `x: f64, y: f64, z: f64, w: f64` | 32 B | | `Twist` | `linear: [f64; 3], angular: [f64; 3]` | 48 B | | `Accel` | `linear: [f64; 3], angular: [f64; 3]` | 48 B | | `TransformStamped` | `parent: str, child: str, timestamp_ns: u64, transform: Transform` | var | | `PoseStamped` | `pose: Pose3D, timestamp_ns: u64, frame_id: str` | var | | `PoseWithCovariance` | `pose: Pose3D, covariance: [f64; 36]` | 344 B | | `TwistWithCovariance` | `twist: Twist, covariance: [f64; 36]` | 336 B | | `AccelStamped` | `accel: Accel, timestamp_ns: u64, frame_id: str` | var | ### Sensors | Type | Fields | Size | |------|--------|------| | `Imu` | `orientation: [f64;4], angular_velocity: [f64;3], linear_acceleration: [f64;3]` | 80 B | | `LaserScan` | `angle_min/max: f32, angle_increment: f32, ranges: Vec, intensities: Vec` | var | | `Odometry` | `pose: Pose2D, twist: Twist, timestamp_ns: u64` | var | | `JointState` | `position: Vec, velocity: Vec, effort: Vec, name: Vec` | var | | `BatteryState` | `voltage: f32, percentage: f32, current: f32, charging: bool, temperature: f32` | 17 B | | `RangeSensor` | `sensor_type: u8, range: f32, min_range: f32, max_range: f32, fov: f32` | 17 B | | `NavSatFix` | `latitude: f64, longitude: f64, altitude: f64, status: i8, covariance: [f64;9]` | var | | `MagneticField` | `field: [f64; 3], covariance: [f64; 9]` | 96 B | | `Temperature` | `temperature: f64, variance: f64` | 16 B | | `FluidPressure` | `pressure: f64, variance: f64` | 16 B | | `Illuminance` | `illuminance: f64, variance: f64` | 16 B | ### Control | Type | Fields | Size | |------|--------|------| | `CmdVel` | `linear: f32, angular: f32` | 8 B | | `MotorCommand` | `left: f64, right: f64` | 16 B | | `ServoCommand` | `servo_id: u8, position: f32, speed: f32, torque_limit: f32` | 13 B | | `JointCommand` | `name: Vec, position: Vec, velocity: Vec, effort: Vec` | var | | `PidConfig` | `kp: f64, ki: f64, kd: f64, output_min: f64, output_max: f64` | 40 B | | `TrajectoryPoint` | `position: [f64;3], linear_velocity: Vec3, angular_velocity: Vec3, time: f64` | var | | `DifferentialDriveCommand` | `left: f64, right: f64, timestamp_ns: u64` | 24 B | ### Navigation | Type | Fields | Size | |------|--------|------| | `NavGoal` | `target_pose: Pose2D, position_tolerance: f64, angle_tolerance: f64` | 40 B | | `GoalResult` | `goal_id: u32, status: GoalStatus, message: String` | var | | `NavPath` | `waypoints: Vec` | var | | `PathPlan` | `poses: Vec, cost: f64` | var | | `Waypoint` | `pose: Pose2D, speed: f64, tolerance: f64` | 40 B | | `OccupancyGrid` | `width: u32, height: u32, resolution: f32, origin: Pose2D, data: Vec` | var | | `CostMap` | `width: u32, height: u32, resolution: f32, origin: Pose2D, data: Vec` | var | | `VelocityObstacle` | `position: Point3, velocity: Vector3, radius: f64` | 56 B | ### Vision | Type | Fields | |------|--------| | `CompressedImage` | `format: String, data: Vec, timestamp_ns: u64` | | `CameraInfo` | `width: u32, height: u32, fx: f64, fy: f64, cx: f64, cy: f64, distortion: Vec` | | `RegionOfInterest` | `x: u32, y: u32, width: u32, height: u32` | | `StereoInfo` | `left: CameraInfo, right: CameraInfo, baseline: f64` | ### Perception and Detection | Type | Fields | |------|--------| | `BoundingBox2D` | `x: f32, y: f32, width: f32, height: f32` | | `BoundingBox3D` | `cx: f32, cy: f32, cz: f32, length: f32, width: f32, height: f32, yaw: f32` | | `Detection` | `class_name: String, confidence: f32, bbox: BoundingBox2D` | | `Detection3D` | `class_name: String, confidence: f32, bbox: BoundingBox3D` | | `Landmark` | `x: f32, y: f32, visibility: f32, index: u32` | | `Landmark3D` | `x: f32, y: f32, z: f32, visibility: f32, index: u32` | | `LandmarkArray` | `landmarks: Vec, landmarks_3d: Vec` | | `SegmentationMask` | `width: u32, height: u32, class_ids: Vec` | | `TrackedObject` | `track_id: u64, bbox: BoundingBox2D, class_id: u32, confidence: f32` | | `PlaneDetection` | `coefficients: [f64;4], center: Point3, normal: Vector3` | ### Force and Haptics | Type | Fields | |------|--------| | `WrenchStamped` | `force: Vector3, torque: Vector3, timestamp_ns: u64, frame_id: String` | | `ForceCommand` | `force: Vector3, torque: Vector3, frame_id: String` | | `ImpedanceParameters` | `stiffness: [f64;6], damping: [f64;6], inertia: [f64;6]` | | `HapticFeedback` | `force: Vector3, vibration_frequency: f64, vibration_amplitude: f64` | | `ContactInfo` | `state: ContactState, force_magnitude: f64, contact_point: Point3, normal: Vector3` | ### Diagnostics | Type | Fields | |------|--------| | `Heartbeat` | `node_name: String, node_id: u32, timestamp_ns: u64, sequence: u64, health: HealthStatus` | | `DiagnosticStatus` | `level: StatusLevel, code: u32, message: String, values: Vec` | | `DiagnosticReport` | `statuses: Vec, timestamp_ns: u64` | | `EmergencyStop` | `triggered: bool, source: String, reason: String, timestamp_ns: u64` | | `SafetyStatus` | `state: NodeStateMsg, health: HealthStatus, violations: Vec` | | `ResourceUsage` | `cpu_percent: f32, memory_bytes: u64, thread_count: u32` | | `NodeHeartbeat` | `node_name: String, tick_count: u64, health: HealthStatus` | ### Input | Type | Fields | |------|--------| | `JoystickInput` | `joystick_id: u32, input_type: InputType, button_id/axis_id: u32, value: f32` | | `KeyboardInput` | `key: String, code: u32, modifiers: Vec, pressed: bool` | ### Clock | Type | Fields | |------|--------| | `Clock` | `timestamp_ns: u64, clock_type: ClockType` | | `TimeReference` | `time_ref_ns: u64, source_name: String, offset_ns: i64` | ## Python API ### Node (Functional) | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `name` | `Optional[str]` | Auto-generated UUID | Unique node name | | `subs` | `str`, `list`, or `dict` | `None` | Topics to subscribe to | | `pubs` | `str`, `list`, or `dict` | `None` | Topics to publish to | | `tick` | `Callable[[Node], None]` | `None` | Main loop callback (can be `async def`) | | `init` | `Callable[[Node], None]` | `None` | Startup callback | | `shutdown` | `Callable[[Node], None]` | `None` | Cleanup callback | | `on_error` | `Callable[[Node, Exception], None]` | `None` | Error handler | | `rate` | `float` | `30` | Tick rate in Hz | ### Node Methods | Method | Signature | Description | |--------|-----------|-------------| | `has_msg` | `has_msg(topic: str) -> bool` | Check if messages available (peeks) | | `recv` | `recv(topic: str) -> Optional[Any]` | Receive next message | | `recv_all` | `recv_all(topic: str) -> List[Any]` | Drain all available messages | | `send` | `send(topic: str, data: Any) -> bool` | Send data to topic | | `log_info` | `log_info(message: str) -> None` | Log info (during tick only) | | `log_warning` | `log_warning(message: str) -> None` | Log warning (during tick only) | | `log_error` | `log_error(message: str) -> None` | Log error (during tick only) | | `log_debug` | `log_debug(message: str) -> None` | Log debug (during tick only) | | `request_stop` | `request_stop() -> None` | Request scheduler shutdown | | `publishers` | `publishers() -> List[str]` | List pub topic names | | `subscribers` | `subscribers() -> List[str]` | List sub topic names | ### Scheduler | Method | Signature | Description | |--------|-----------|-------------| | `__init__` | `Scheduler(*, tick_rate=1000.0, rt=False, deterministic=False, blackbox_mb=0, watchdog_ms=0, recording=False)` | Constructor | | `add` | `add(node, order=100, rate=None, rt=False, failure_policy=None, on_miss=None, budget=None, deadline=None) -> Scheduler` | Add node with options | | `node` | `node(node) -> NodeBuilder` | Fluent builder API | | `run` | `run(duration=None) -> None` | Run scheduler (blocks). Pass seconds or `None` for forever | | `stop` | `stop() -> None` | Signal stop | | `set_node_rate` | `set_node_rate(name: str, rate: float) -> None` | Change node rate at runtime | | `get_node_stats` | `get_node_stats(name: str) -> Dict` | Per-node metrics | | `get_all_nodes` | `get_all_nodes() -> List[Dict]` | All node metrics | | `get_node_count` | `get_node_count() -> int` | Number of registered nodes | | `has_node` | `has_node(name: str) -> bool` | Check node exists | | `get_node_names` | `get_node_names() -> List[str]` | All node names | | `status` | `status() -> str` | Scheduler status string | | `capabilities` | `capabilities() -> Optional[Dict]` | RT capabilities | | `has_full_rt` | `has_full_rt() -> bool` | Full RT available? | | `safety_stats` | `safety_stats() -> Optional[Dict]` | Budget overruns, deadline misses | | `current_tick` | `current_tick() -> int` | Tick counter | ### `horus.run()` | Parameter | Type | Description | |-----------|------|-------------| | `*nodes` | `Node` | Node instances to run (positional) | | `duration` | `Optional[float]` | Seconds to run (`None` = forever) | | `tick_rate` | `float` | Global tick rate in Hz (default: 1000.0) | | `deterministic` | `bool` | SimClock, fixed dt, seeded RNG | | `rt` | `bool` | Enable memory locking + RT scheduling | | `watchdog_ms` | `int` | Watchdog timeout (0 = disabled) | | `blackbox_mb` | `int` | Flight recorder size (0 = disabled) | | `recording` | `bool` | Enable session recording | ## CLI Commands | Command | Usage | Description | |---------|-------|-------------| | `horus new` | `horus new mybot [--python\|--rust\|--cpp]` | Create a new project | | `horus init` | `horus init [-n name]` | Initialize workspace in current directory | | `horus run` | `horus run [files...] [-r] [--record name]` | Build and run project | | `horus build` | `horus build [files...] [-r] [-c]` | Build without running | | `horus test` | `horus test [FILTER] [-r] [--sim] [--integration]` | Run tests | | `horus check` | `horus check [PATH] [--full] [--health]` | Validate manifest and sources | | `horus clean` | `horus clean [--shm] [--all] [-n]` | Clean build artifacts and SHM | | `horus launch` | `horus launch file.yaml [--dry-run]` | Launch multiple nodes from YAML | | `horus topic` | `horus topic list\|echo\|pub\|info` | Topic introspection | | `horus node` | `horus node list\|info\|kill` | Node management | | `horus param` | `horus param get\|set\|list\|delete` | Runtime parameters | | `horus frame` | `horus frame list\|echo\|tree` | TransformFrame operations (alias: `horus tf`) | | `horus service` | `horus service list\|call\|info` | Service interaction | | `horus action` | `horus action list\|info\|send-goal\|cancel-goal` | Action interaction | | `horus msg` | `horus msg list\|show\|fields` | Message type introspection | | `horus log` | `horus log [NODE] [-l level] [-f] [-n N]` | View and filter logs | | `horus blackbox` | `horus blackbox [-a] [-f] [--json]` | Flight recorder inspection | | `horus monitor` | `horus monitor [PORT] [--tui]` | Live TUI/web dashboard | | `horus install` | `horus install name[@ver] [--driver\|--plugin]` | Install package or driver | --- ## AI Context — Complete Framework Reference Path: /reference/ai-context Description: Dense, structured reference for AI agents. Complete API surface, safety rules, patterns, and examples in one page. # AI Context — Complete Framework Reference ## Mental Model HORUS is a tick-based robotics runtime. Applications are composed of **Nodes** (units of computation) that exchange data through **Topics** (typed pub/sub channels backed by shared memory). A **Scheduler** orchestrates node execution in priority order each tick cycle. Communication is local shared memory IPC with latencies from ~3ns (same-thread) to ~167ns (cross-process). Systems can be single-process (all nodes in one scheduler) or multi-process (nodes in separate processes sharing topics via `/dev/shm/horus/`). There are no callbacks — nodes implement `tick()` which the scheduler calls each cycle. --- ## Node Trait (Complete) All nodes implement the `Node` trait. Only `tick()` is required; all others have defaults. ```rust use horus::prelude::*; ``` | Method | Signature | Default | Description | |--------|-----------|---------|-------------| | `name` | `fn name(&self) -> &str` | **Required** | Unique node identifier | | `tick` | `fn tick(&mut self)` | **Required** | Called every scheduler cycle; do work here | | `init` | `fn init(&mut self) -> Result<()>` | `Ok(())` | Called once before first tick; open hardware, allocate buffers | | `shutdown` | `fn shutdown(&mut self) -> Result<()>` | `Ok(())` | Called on Ctrl+C / scheduler stop; release hardware, zero actuators | | `publishers` | `fn publishers(&self) -> Vec` | `vec![]` | Declare published topic names for introspection | | `subscribers` | `fn subscribers(&self) -> Vec` | `vec![]` | Declare subscribed topic names for introspection | | `on_error` | `fn on_error(&mut self, error: &Error)` | no-op | Called when tick() panics or returns error | | `is_safe_state` | `fn is_safe_state(&self) -> bool` | `true` | Query whether node is in a safe state | | `enter_safe_state` | `fn enter_safe_state(&mut self)` | no-op | Called by safety monitor on deadline miss with `Miss::SafeMode` | --- ## NodeBuilder API (Complete) Chain after `scheduler.add(node)`. Finalize with `.build()`. | Method | Parameter | Effect | |--------|-----------|--------| | `.order(n)` | `u32` | Execution priority; lower = runs first. 0-9 critical, 10-49 high, 50-99 normal, 100-199 low, 200+ background | | `.rate(freq)` | `Frequency` | Per-node tick rate. Auto-derives budget (80% period), deadline (95% period). Auto-marks as RT | | `.budget(dur)` | `Duration` | Max allowed tick execution time. Auto-marks as RT | | `.deadline(dur)` | `Duration` | Hard deadline for tick completion. Auto-marks as RT | | `.on_miss(policy)` | `Miss` | Deadline miss policy: `Warn`, `Skip`, `SafeMode`, `Stop` | | `.compute()` | — | Mark as CPU-heavy compute node (may use worker threads) | | `.on(topic)` | `&str` | Event-driven: node ticks only when topic receives a message | | `.async_io()` | — | Mark as async I/O node (non-blocking network/file) | | `.monitoring(bool)` | `bool` | Enable per-node monitoring | | `.prefer_rt()` | — | Request RT features, degrade gracefully | | `.require_rt()` | — | Require RT features, panic if unavailable | | `.deterministic(bool)` | `bool` | Enable deterministic execution mode | | `.max_deadline_misses(n)` | `u32` | Emergency stop after n misses | | `.cores(list)` | `&[usize]` | Pin node to specific CPU cores | | `.with_profiling()` | — | Enable per-node profiling | | `.with_blackbox(mb)` | `usize` | Per-node blackbox buffer in MB | | `.verbose(bool)` | `bool` | Enable/disable logging for this node | | `.failure_policy(p)` | `FailurePolicy` | Override failure handling: `Fatal`, `Restart`, `Skip`, `Ignore` | | `.build()` | — | Finalize and register the node. Returns `Result<()>` | **RT auto-detection**: Setting `.budget()`, `.deadline()`, or `.rate(Frequency)` automatically sets `is_rt=true` and `ExecutionClass::Rt`. There is no `.rt()` method. This is order-independent via deferred `finalize()`. --- ## Topic API (Complete) `Topic` requires `T: Clone + Serialize + Deserialize + Send + Sync + 'static`. | Method | Signature | Behavior | |--------|-----------|----------| | `new` | `Topic::new(name: &str) -> Result` | Create/connect to a named topic. Auto-selects optimal IPC backend | | `with_capacity` | `Topic::with_capacity(name: &str, capacity: u32, slot_size: Option) -> Result` | Custom ring buffer capacity | | `send` | `fn send(&self, msg: T)` | Publish message. Infallible. Overwrites oldest on buffer full | | `try_send` | `fn try_send(&self, msg: T) -> Result<(), T>` | Attempt send; returns message on failure | | `send_blocking` | `fn send_blocking(&self, msg: T, timeout: Duration) -> Result<(), SendBlockingError>` | Block until space available or timeout. For critical commands | | `recv` | `fn recv(&self) -> Option` | Non-blocking receive. Returns `None` if empty | | `try_recv` | `fn try_recv(&self) -> Option` | Same as `recv()` for most use cases | | `read_latest` | `fn read_latest(&self) -> Option where T: Copy` | Read latest without advancing consumer position. Requires `T: Copy` | | `has_message` | `fn has_message(&self) -> bool` | Check if messages are available without consuming | | `pending_count` | `fn pending_count(&self) -> u64` | Number of messages waiting | | `name` | `fn name(&self) -> &str` | Topic name | | `metrics` | `fn metrics(&self) -> TopicMetrics` | Message counts, send/recv failures | | `dropped_count` | `fn dropped_count(&self) -> u64` | Messages lost to buffer overflow | --- ## Scheduler API | Method | Signature | Description | |--------|-----------|-------------| | `new` | `Scheduler::new() -> Scheduler` | Create with auto-detected capabilities (~30-100us) | | `.tick_rate(freq)` | `Frequency` | Global tick rate (default: 100 Hz) | | `.prefer_rt()` | — | Try RT features, degrade gracefully | | `.require_rt()` | — | Enable RT features, panic if unavailable | | `.watchdog(dur)` | `Duration` | Frozen node detection, auto-creates safety monitor | | `.deterministic(bool)` | `bool` | Enable deterministic mode | | `.with_blackbox(mb)` | `usize` | BlackBox flight recorder buffer | | `.with_recording()` | — | Enable record/replay | | `.max_deadline_misses(n)` | `u32` | Emergency stop after n deadline misses (default: 100) | | `.verbose(bool)` | `bool` | Enable/disable non-emergency logging | | `.add(node)` | `impl Node` | Add node, returns chainable builder | | `.run()` | `-> Result<()>` | Main loop until Ctrl+C | | `.run_for(dur)` | `Duration -> Result<()>` | Run for specific duration | | `.tick_once()` | `-> Result<()>` | Execute exactly one tick cycle | | `.tick(names)` | `&[&str] -> Result<()>` | One tick cycle for named nodes only | | `.set_node_rate(name, freq)` | `&str, Frequency` | Change per-node rate at runtime | | `.stop()` | — | Stop the scheduler | | `.is_running()` | `-> bool` | Check if running | | `.metrics()` | `-> Vec` | Per-node performance metrics | | `.safety_stats()` | `-> Option` | Budget overruns, deadline misses, watchdog expirations | | `.node_list()` | `-> Vec` | Registered node names | --- ## DurationExt and Frequency | Method | Type | Example | Result | |--------|------|---------|--------| | `.ns()` | `Duration` | `500.ns()` | 500 nanoseconds | | `.us()` | `Duration` | `200.us()` | 200 microseconds | | `.ms()` | `Duration` | `10.ms()` | 10 milliseconds | | `.secs()` | `Duration` | `1.secs()` | 1 second | | `.hz()` | `Frequency` | `100.hz()` | 100 Hz (period = 10ms) | **Frequency methods**: `value() -> f64`, `period() -> Duration`, `budget_default() -> Duration` (80% period), `deadline_default() -> Duration` (95% period). **Validation**: `Frequency` panics on 0, negative, NaN, or infinity. Works on `u64`, `f64`, `i32`. Import via `use horus::prelude::*;`. --- ## Execution Classes | Class | When Used | Thread Model | How Triggered | |-------|-----------|--------------|---------------| | **Rt** | Real-time nodes with timing guarantees | Priority-scheduled, optional CPU pinning | Auto: set `.budget()`, `.deadline()`, or `.rate()` | | **Compute** | CPU-heavy work (ML inference, path planning) | May use worker thread pool | `.compute()` | | **Event** | React to incoming data | Wakes on topic message | `.on("topic.name")` | | **AsyncIo** | Network, file, database I/O | Non-blocking async runtime | `.async_io()` | | **BestEffort** | Default; no special scheduling | Runs in tick order | No method called (default) | Execution classes are **mutually exclusive** per node. RT is always implicit from timing parameters. --- ## Miss (Deadline Policy) | Policy | What Happens | Use For | |--------|--------------|---------| | `Miss::Warn` | Log warning, continue normally | Soft RT (logging, UI). **Default** | | `Miss::Skip` | Skip this node for current tick | Firm RT (video encoding) | | `Miss::SafeMode` | Call `enter_safe_state()` on the node | Motor controllers, safety nodes | | `Miss::Stop` | Stop entire scheduler | Hard RT safety-critical | --- ## Safety Rules (CRITICAL) **Rule one: Always implement `shutdown()` for actuators.** Without it, motors continue at last velocity on Ctrl+C. ```rust fn shutdown(&mut self) -> Result<()> { self.motor.set_velocity(0.0); Ok(()) } ``` **Rule two: Always call `recv()` every tick.** Ring buffers overwrite old messages. Skipping ticks loses data. ```rust fn tick(&mut self) { // ALWAYS recv, cache result if let Some(msg) = self.sub.recv() { self.cached = Some(msg); } // Then use cached value } ``` **Rule three: Never `sleep()` in `tick()`.** It blocks the entire scheduler. All nodes share the tick cycle. ```rust // BAD: std::thread::sleep(Duration::from_millis(100)); // GOOD: Use scheduler rate control instead ``` **Rule four: Never do blocking I/O in `tick()`.** File reads, network calls, and database queries belong in `init()` or in an `.async_io()` node. ```rust // BAD: let data = std::fs::read_to_string("file.txt")?; // GOOD: Read in init(), use cached data in tick() ``` **Rule five: Use dots not slashes in topic names.** Slashes conflict with shared memory paths and fail on macOS. ```rust // CORRECT: Topic::new("sensors.lidar") // WRONG: Topic::new("sensors/lidar") ``` --- ## Common Patterns ### Pattern: Publisher ```rust use horus::prelude::*; struct TempSensor { pub_topic: Topic, } impl TempSensor { fn new() -> Result { Ok(Self { pub_topic: Topic::new("sensor.temperature")? }) } } impl Node for TempSensor { fn name(&self) -> &str { "TempSensor" } fn tick(&mut self) { self.pub_topic.send(self.read_temperature()); } } ``` ### Pattern: Subscriber ```rust use horus::prelude::*; struct Logger { sub: Topic, } impl Logger { fn new() -> Result { Ok(Self { sub: Topic::new("sensor.temperature")? }) } } impl Node for Logger { fn name(&self) -> &str { "Logger" } fn tick(&mut self) { if let Some(temp) = self.sub.recv() { hlog!(info, "Temperature: {:.1}C", temp); } } } ``` ### Pattern: Pub+Sub Pipeline ```rust use horus::prelude::*; struct Filter { scan_sub: Topic, cmd_pub: Topic, } impl Filter { fn new() -> Result { Ok(Self { scan_sub: Topic::new("scan")?, cmd_pub: Topic::new("cmd_vel")?, }) } } impl Node for Filter { fn name(&self) -> &str { "Filter" } fn tick(&mut self) { if let Some(scan) = self.scan_sub.recv() { if let Some(min) = scan.min_range() { if min < 0.5 { self.cmd_pub.send(CmdVel::zero()); } else { self.cmd_pub.send(CmdVel::new(1.0, 0.0)); } } } } } ``` ### Pattern: Multi-Rate System ```rust use horus::prelude::*; fn main() -> Result<()> { let mut scheduler = Scheduler::new() .tick_rate(1000.hz()) .watchdog(500.ms()); scheduler.add(ImuSensor::new()?).order(0).rate(100.hz()).build()?; scheduler.add(PidController::new()?).order(10).rate(50.hz()) .budget(500.us()).on_miss(Miss::Skip).build()?; scheduler.add(PathPlanner::new()?).order(50).rate(10.hz()) .compute().build()?; scheduler.add(TelemetryLogger::new()?).order(200).rate(1.hz()) .async_io().build()?; scheduler.run() } ``` ### Pattern: State Machine Always call `recv()` unconditionally, outside the state match. ```rust use horus::prelude::*; struct StateMachine { cmd_sub: Topic, motor_pub: Topic, state: State, last_cmd: Option, } enum State { Idle, Moving, Stopping } impl Node for StateMachine { fn name(&self) -> &str { "StateMachine" } fn tick(&mut self) { // ALWAYS recv first, regardless of state if let Some(cmd) = self.cmd_sub.recv() { self.last_cmd = Some(cmd); } match self.state { State::Idle => { if let Some(ref cmd) = self.last_cmd { if cmd.linear.abs() > 0.01 { self.state = State::Moving; } } } State::Moving => { if let Some(ref cmd) = self.last_cmd { self.motor_pub.send(MotorCommand::from_cmd_vel(cmd)); if cmd.linear.abs() < 0.01 { self.state = State::Stopping; } } } State::Stopping => { self.motor_pub.send(MotorCommand::stop()); self.state = State::Idle; self.last_cmd = None; } } } fn shutdown(&mut self) -> Result<()> { self.motor_pub.send(MotorCommand::stop()); Ok(()) } } ``` ### Pattern: Aggregator with Caching Synchronize multiple topics by caching the latest value from each. ```rust use horus::prelude::*; struct Aggregator { imu_sub: Topic, odom_sub: Topic, fused_pub: Topic, last_imu: Option, last_odom: Option, } impl Node for Aggregator { fn name(&self) -> &str { "Aggregator" } fn tick(&mut self) { // Always drain both topics if let Some(imu) = self.imu_sub.recv() { self.last_imu = Some(imu); } if let Some(odom) = self.odom_sub.recv() { self.last_odom = Some(odom); } // Fuse when both are available if let (Some(ref imu), Some(ref odom)) = (&self.last_imu, &self.last_odom) { let fused = self.fuse(imu, odom); self.fused_pub.send(fused); } } } ``` --- ## Standard Message Types All types available via `use horus::prelude::*;`. All fixed-size types support zero-copy shared memory transport. ### Geometry | Type | Key Fields | |------|------------| | `Twist` | `linear: [f64; 3]`, `angular: [f64; 3]`, `timestamp_ns: u64` | | `CmdVel` | `linear: f32`, `angular: f32`, `timestamp_ns: u64` | | `Pose2D` | `x: f64`, `y: f64`, `theta: f64`, `timestamp_ns: u64` | | `Pose3D` | `position: Point3`, `orientation: Quaternion`, `timestamp_ns: u64` | | `PoseStamped` | `pose: Pose3D`, `frame_id: [u8; 32]` | | `PoseWithCovariance` | `pose: Pose3D`, `covariance: [f64; 36]` | | `TwistWithCovariance` | `twist: Twist`, `covariance: [f64; 36]` | | `Point3` | `x: f64`, `y: f64`, `z: f64` | | `Vector3` | `x: f64`, `y: f64`, `z: f64` | | `Quaternion` | `x: f64`, `y: f64`, `z: f64`, `w: f64` | | `TransformStamped` | `translation: [f64; 3]`, `rotation: [f64; 4]` | | `Accel` / `AccelStamped` | `linear: [f64; 3]`, `angular: [f64; 3]` | ### Sensors | Type | Key Fields | |------|------------| | `Imu` | `orientation: [f64; 4]`, `angular_velocity: [f64; 3]`, `linear_acceleration: [f64; 3]`, covariance arrays | | `LaserScan` | `ranges: [f32; 360]`, `angle_min/max: f32`, `range_min/max: f32` | | `Odometry` | `pose: Pose2D`, `twist: Twist`, covariance arrays, frame IDs | | `JointState` | `names: [[u8;32];16]`, `positions/velocities/efforts: [f64;16]`, `joint_count: u8` | | `NavSatFix` | `latitude/longitude/altitude: f64`, `status: u8`, `satellites_visible: u16` | | `BatteryState` | `voltage/current/charge/capacity: f32`, `percentage: f32`, `temperature: f32` | | `RangeSensor` | `range: f32`, `sensor_type: u8`, `field_of_view: f32` | | `Temperature` | `temperature: f64`, `variance: f64` | | `FluidPressure` | `fluid_pressure: f64`, `variance: f64` | | `Illuminance` | `illuminance: f64`, `variance: f64` | | `MagneticField` | `magnetic_field: [f64; 3]`, `covariance: [f64; 9]` | ### Control | Type | Key Fields | |------|------------| | `MotorCommand` | `motor_id: u8`, `mode: u8` (0=vel,1=pos,2=torque,3=voltage), `target: f64` | | `ServoCommand` | `servo_id: u8`, `position: f32` (rad), `speed: f32` (0-1) | | `JointCommand` | Joint-level position/velocity/torque commands | | `PidConfig` | `kp/ki/kd: f64`, `integral_limit: f64`, `output_limit: f64` | | `DifferentialDriveCommand` | `left_velocity: f64`, `right_velocity: f64` (rad/s) | | `TrajectoryPoint` | `position: [f64;3]`, `velocity: [f64;3]`, `orientation: [f64;4]`, `time_from_start: f64` | ### Navigation | Type | Key Fields | |------|------------| | `NavGoal` | `target_pose: Pose2D`, `tolerance_position/angle: f64`, `timeout_seconds: f64` | | `NavPath` | `waypoints: [Waypoint; 256]`, `waypoint_count: u16`, `total_length: f64` | | `OccupancyGrid` | Grid-based occupancy map (variable-size) | | `CostMap` | Navigation cost map (variable-size) | | `PathPlan` | Planned path with algorithm metadata | ### Vision | Type | Key Fields | |------|------------| | `Image` | Pool-backed RAII. `Image::new(w, h, encoding)?`. Zero-copy via shared memory pool | | `DepthImage` | Pool-backed RAII. F32 or U16 depth data | | `CompressedImage` | `format: [u8;8]`, `data: Vec` (variable-size, MessagePack serialized) | | `CameraInfo` | `width/height: u32`, `camera_matrix: [f64;9]`, `distortion_coefficients: [f64;8]` | | `RegionOfInterest` | `x_offset/y_offset/width/height: u32`, `do_rectify: bool` | ### Perception | Type | Key Fields | |------|------------| | `Detection` | `bbox: BoundingBox2D`, `confidence: f32`, `class_id: u32` | | `Detection3D` | `bbox: BoundingBox3D`, `confidence: f32`, `velocity_x/y/z: f32` | | `BoundingBox2D` | `x/y/width/height: f32` (pixels) | | `BoundingBox3D` | `cx/cy/cz/length/width/height: f32` (meters), `roll/pitch/yaw: f32` | | `PointCloud` | Pool-backed RAII. `PointXYZ`, `PointXYZI`, `PointXYZRGB` formats | | `Landmark` / `Landmark3D` | `x/y(/z): f32`, `visibility: f32`, `index: u32` | | `LandmarkArray` | Up to N landmarks. Presets: `coco_pose()`, `mediapipe_pose/hand/face()` | | `SegmentationMask` | `width/height: u32`, `num_classes: u32`, `mask_type: u32` | | `PlaneDetection` | `coefficients: [f64;4]`, `center: Point3`, `normal: Vector3` | | `TrackedObject` | Object tracking with ID and velocity | ### Force and Impedance | Type | Key Fields | |------|------------| | `WrenchStamped` | `force: [f64;3]`, `torque: [f64;3]`, `frame_id` | | `ForceCommand` | Force/torque command for compliant control | | `ImpedanceParameters` | Stiffness, damping, inertia for impedance control | ### Diagnostics | Type | Key Fields | |------|------------| | `DiagnosticReport` | `component: [u8;32]`, up to 16 `DiagnosticValue` entries | | `DiagnosticStatus` | Status level + message | | `DiagnosticValue` | Typed key-value: `string()`, `int()`, `float()`, `bool()` | | `EmergencyStop` | Emergency stop command | | `Heartbeat` / `NodeHeartbeat` | Periodic health signal with tick count and rate | | `SafetyStatus` | Overall system safety state | | `ResourceUsage` | CPU, memory, disk usage | ### Input | Type | Key Fields | |------|------------| | `JoystickInput` | Axes, buttons, hat switches for teleoperation | | `KeyboardInput` | Key events for HID control | ### Clock | Type | Key Fields | |------|------------| | `Clock` | Simulation/wall time. Sources: `SOURCE_WALL`, `SOURCE_SIM`, `SOURCE_REPLAY` | | `TimeReference` | Time synchronization reference | --- ## Python API Quick Reference ### Functional Node ```python import horus def my_tick(node): node.send("temperature", 25.5) sensor = horus.Node( name="temp_sensor", pubs="temperature", tick=my_tick, rate=10 ) horus.run(sensor, duration=30) ``` ### Stateful Node (class container) ```python import horus from horus import CmdVel class DriveState: def tick(self, node): node.send("cmd_vel", CmdVel(linear=0.5, angular=0.0)) def shutdown(self, node): node.send("cmd_vel", CmdVel(linear=0.0, angular=0.0)) drive = DriveState() node = horus.Node(name="drive_node", tick=drive.tick, shutdown=drive.shutdown, pubs=["cmd_vel"], rate=50, order=0) ``` ### Run ```python horus.run(node) ``` ### One-Liner ```python horus.run(sensor_node, controller_node, duration=60) ``` ### Topic ```python from horus import Topic, CmdVel pub = Topic("cmd_vel", CmdVel) pub.send(CmdVel(linear=1.0, angular=0.0)) sub = Topic("cmd_vel", CmdVel) msg = sub.recv() # Returns None if empty ``` ### Functional recv ```python def process(node): if node.has_msg("scan"): scan = node.recv("scan") all_scans = node.recv_all("scan") ``` --- ## CLI Quick Reference | Command | Usage | |---------|-------| | `horus new ` | Create new project with `horus.toml` + `src/` | | `horus run [files...]` | Build and run application | | `horus build` | Build without running | | `horus test [filter]` | Run tests | | `horus check` | Validate `horus.toml` and workspace | | `horus clean --shm` | Clean stale shared memory regions | | `horus monitor` | Web + TUI monitoring dashboard | | `horus topic list` | List active topics | | `horus topic echo ` | Print messages on a topic | | `horus node list` | List running nodes | | `horus tf tree` | Print transform frame tree | | `horus install ` | Install package from registry | | `horus launch ` | Launch multi-node system from YAML | | `horus param get ` | Get runtime parameter | | `horus deploy [target]` | Deploy to remote robot | | `horus doctor` | Comprehensive health check | | `horus fmt` | Format code (Rust + Python) | | `horus lint` | Lint code (clippy + ruff) | --- ## horus.toml Format `horus.toml` is the single source of truth. Native build files (`Cargo.toml`, `pyproject.toml`) are generated into `.horus/` automatically. ```toml [package] name = "my-robot" version = "0.1.0" description = "My robot project" authors = ["Name "] [dependencies] # Rust deps (auto-detected as crates.io) serde = { version = "1.0", source = "crates.io", features = ["derive"] } nalgebra = "0.32" # Python deps (specify source = "pypi") numpy = { version = ">=1.24", source = "pypi" } torch = { version = ">=2.0", source = "pypi" } # System deps libudev = { version = "*", source = "system" } # Path deps my_lib = { path = "../my_lib" } # Git deps some_crate = { git = "https://github.com/user/repo", branch = "main" } [dev-dependencies] criterion = { version = "0.5", source = "crates.io" } pytest = { version = ">=7.0", source = "pypi" } [scripts] sim = "horus sim start --world warehouse" deploy = "horus deploy pi@robot --release" test-hw = "horus run tests/hardware_check.rs" [drivers] realsense = { version = "0.3" } dynamixel = { version = "0.2" } [hooks] pre_run = ["fmt", "lint"] post_build = ["test"] ``` **Dependency sources**: `crates.io` (Rust, default), `pypi` (Python), `system`, `path`, `git`. **Generated files**: `horus build` creates `.horus/Cargo.toml` and `.horus/pyproject.toml`. Never edit these directly. --- ## Import Pattern ```rust use horus::prelude::*; // Provides: Node, Topic, Scheduler, DurationExt, Frequency, Miss, // all message types, error types, macros, services, actions, etc. // 165+ types in one import. ``` ## Custom Messages ```rust use serde::{Serialize, Deserialize}; #[derive(Clone, Serialize, Deserialize)] struct MyMessage { x: f32, y: f32, label: String, } let topic: Topic = Topic::new("my.data")?; ``` ## Macros | Macro | Purpose | |-------|---------| | `message!` | Define custom message types | | `service!` | Define request/response service types | | `action!` | Define long-running action types (goal/feedback/result) | | `node!` | Define node with automatic topic registration | | `topics!` | Compile-time topic name + type descriptors | | `hlog!(level, ...)` | Structured node logging | | `hlog_once!(level, ...)` | Log once per execution | | `hlog_every!(n, level, ...)` | Throttled logging every n calls | ## Error Types ```rust use horus::prelude::*; // Error, Result, HorusError // Variants: CommunicationError, ConfigError, MemoryError, NodeError, // NotFoundError, ParseError, ResourceError, SerializationError, // TimeoutError, TransformError, ValidationError // Helpers: retry_transient(), RetryConfig ``` ## Performance Reference | Metric | Value | |--------|-------| | Same-thread topic | ~3 ns | | Same-process 1:1 | ~18 ns | | Same-process N:M | ~36 ns | | Cross-process | ~50-167 ns | | Scheduler tick overhead | ~50-100 ns | | Shared memory allocation | ~100 ns | --- ## API Reference Path: /reference Description: Complete API reference for HORUS — pure lookup tables for every method, type, and configuration option # API Reference Pure lookup — find the method signature you need. For explanations and tutorials, see [Learn](/learn) and [Tutorials](/tutorials). ## Core APIs | API | What it covers | |-----|---------------| | **[Scheduler API](/rust/api/scheduler)** | Builder methods, execution modes, introspection | | **[Topic API](/rust/api/topic)** | send, recv, try_send, try_recv, read_latest, capacity | | **[Services API](/rust/api/services)** | ServiceClient, ServiceServer, call, call_resilient | | **[Actions API](/rust/api/actions)** | ActionClient, ActionServer, GoalStatus, GoalOutcome | | **[TransformFrame API](/rust/api/transform-frame)** | Frame registration, lookups, interpolation | ## Helpers | API | What it covers | |-----|---------------| | **[DurationExt & Frequency](/rust/api/duration-ext)** | `.hz()`, `.ms()`, `.us()`, `.secs()`, `Frequency` type | | **[Macros](/rust/api/macros)** | `message!`, `service!`, `action!`, `node!`, `hlog!` | | **[Feature Flags](/rust/api/feature-flags)** | telemetry, blackbox, macros | ## Configuration | Reference | What it covers | |-----------|---------------| | **[CLI Reference](/development/cli-reference)** | All `horus` commands and flags | | **[Error Reference](/development/error-handling)** | HorusError variants, error patterns | | **[horus.toml](/concepts/horus-toml)** | Project manifest format | | **[Configuration](/package-management/configuration)** | Global and project configuration | --- ## Node Discovery & Monitoring Path: /reference/internals Description: Cross-process node discovery and lifecycle monitoring — for building custom monitoring tools # Node Discovery & Monitoring These types are for **building custom monitoring and discovery tools**. Most users don't need them — the built-in [Monitor](/development/monitor) handles this automatically. --- ## NodePresence Node presence files written to shared memory for cross-process discovery. The scheduler creates these automatically at startup; monitoring tools read them. Paths are managed by `horus_sys` and vary by platform. ```rust use horus::prelude::*; // NodePresence is available via the prelude // Discover all running HORUS nodes on this machine let nodes = NodePresence::read_all(); for node in &nodes { println!("{}: pid={}, rate={:?}Hz, health={:?}, ticks={}, errors={}", node.name(), node.pid(), node.rate_hz(), node.health_status(), node.tick_count(), node.error_count()); } // Read a specific node if let Some(motor) = NodePresence::read("motor_ctrl") { println!("Publishers: {:?}", motor.publishers()); println!("Services: {:?}", motor.services()); } ``` | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&str` | Node name | | `pid()` | `u32` | Process ID (verified for liveness) | | `scheduler()` | `Option<&str>` | Scheduler name | | `publishers()` | `&[TopicMetadata]` | Topics this node publishes | | `subscribers()` | `&[TopicMetadata]` | Topics this node subscribes to | | `rate_hz()` | `Option` | Configured tick rate | | `health_status()` | `Option<&str>` | Healthy / Warning / Error / Critical | | `tick_count()` | `u64` | Total ticks since start | | `error_count()` | `u32` | Total errors since start | | `services()` | `&[String]` | Provided services | | `actions()` | `&[String]` | Provided actions | **Static methods:** `read(name) → Option`, `read_all() → Vec` --- ## NodeAnnouncement Real-time lifecycle events broadcast on the `horus.ctl.{scheduler}` topic. Unlike presence files (polled), announcements are push-based — you get notified the moment a node starts or stops. ```rust // discovery module removed — node lifecycle managed via SchedulerRegistry + control topic use horus::prelude::*; let discovery: Topic = Topic::new(DISCOVERY_TOPIC).unwrap(); if let Some(ann) = discovery.recv() { match ann.event { NodeEvent::Started => { println!("{} started (pid={})", ann.name, ann.pid); println!(" publishes: {:?}", ann.publishers); println!(" subscribes: {:?}", ann.subscribers); } NodeEvent::Stopped => println!("{} stopped", ann.name), } } ``` | Field | Type | Description | |-------|------|-------------| | `name` | `String` | Node name | | `pid` | `u32` | Process ID | | `event` | `NodeEvent` | `Started` or `Stopped` | | `publishers` | `Vec` | Published topic names | | `subscribers` | `Vec` | Subscribed topic names | | `timestamp_ms` | `u64` | Event timestamp | --- ## See Also - [Monitor Guide](/development/monitor) — Built-in web + TUI monitoring - [Scheduler API](/rust/api/scheduler) — `ProfileReport` and `status()` - [Topic API](/rust/api/topic) — `TopicMetrics` ======================================== # SECTION: Recipes ======================================== --- ## Recipes Path: /recipes Description: Copy-paste-ready patterns for common robotics tasks. Each recipe is a complete, self-contained program optimized for AI code generation. ## Recipes Complete, production-ready patterns you can copy, adapt, and ship. Unlike tutorials (which teach step-by-step), recipes are pure working programs with inline safety annotations. **Every recipe includes:** - A complete `horus.toml` manifest - A full `src/main.rs` with all imports, types, nodes, and `fn main()` - Inline `// SAFETY:` and `// IMPORTANT:` comments for critical patterns - Expected terminal output ### Available Recipes | Recipe | What It Builds | Key Patterns | |--------|---------------|--------------| | [Differential Drive](/recipes/differential-drive) | 2-wheel robot: `CmdVel` to motor commands | Actuator safety, shutdown, execution order | | [IMU Reader](/recipes/imu-reader) | 100Hz IMU sensor with orientation publishing | Sensor node, `#[repr(C)]`, zero-copy | | [PID Controller](/recipes/pid-controller) | Generic PID loop with configurable gains | Control theory, rate-based RT | | [LiDAR Obstacle Avoidance](/recipes/lidar-obstacle-avoidance) | Reactive velocity from `LaserScan` | Sensor fusion, safety zones | | [Servo Controller](/recipes/servo-controller) | Multi-servo bus with safe shutdown | Multi-actuator, ordered shutdown | | [Multi-Sensor Fusion](/recipes/multi-sensor-fusion) | IMU + odometry state estimation | Multi-topic aggregation, caching | | [Emergency Stop](/recipes/emergency-stop) | E-stop monitor with safety state | Safety-critical, `Miss::SafeMode` | | [Telemetry Logger](/recipes/telemetry-logger) | Log topics to file via async I/O | `async_io()`, non-blocking file writes | | [Python CV Node](/recipes/python-cv-node) | Python computer vision with `horus.Node` | Python API, NumPy integration | | [ROS2 Bridge](/recipes/ros2-bridge) | Bridge horus topics to ROS2 | Multi-process, topic bridging | ### How to Use a Recipe ```bash horus new my-robot -r cd my-robot ``` Copy the `horus.toml` and `src/main.rs` from any recipe, then: ```bash horus run ``` ### Conventions - **Execution order**: Publishers run before subscribers (lower `.order()` first) - **Safety**: Every actuator node implements `shutdown()` that zeros outputs - **recv() every tick**: Always call `.recv()` on every subscriber, every tick — even if you discard the value - **No sleep()**: Never use `std::thread::sleep()` — use `.rate()` on the scheduler - **No blocking I/O in tick()**: Use `.async_io()` for file/network operations --- ## Recipe: Differential Drive Path: /recipes/differential-drive Description: Complete 2-wheel differential drive robot: CmdVel velocity commands to left/right motor outputs with safety shutdown. ## Differential Drive A 2-wheel robot that receives `CmdVel` velocity commands and converts them to left/right motor speeds. Implements safe shutdown — motors are always zeroed on exit. ### horus.toml ```toml [package] name = "differential-drive" version = "0.1.0" description = "2-wheel differential drive with safe shutdown" ``` ### Complete Code ```rust use horus::prelude::*; /// Robot physical parameters const WHEEL_BASE: f32 = 0.3; // meters between wheels const WHEEL_RADIUS: f32 = 0.05; // meters const MAX_RPM: f32 = 200.0; // safety limit /// Motor output for a single wheel #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct WheelCmd { left_rpm: f32, right_rpm: f32, } // ── Drive Node ────────────────────────────────────────────── struct DriveNode { cmd_sub: Topic, wheel_pub: Topic, last_cmd: CmdVel, } impl DriveNode { fn new() -> Result { Ok(Self { cmd_sub: Topic::new("cmd_vel")?, wheel_pub: Topic::new("wheel.cmd")?, last_cmd: CmdVel::default(), }) } } impl Node for DriveNode { fn name(&self) -> &str { "Drive" } fn tick(&mut self) { // IMPORTANT: always recv() every tick to drain the buffer if let Some(cmd) = self.cmd_sub.recv() { self.last_cmd = cmd; } // Differential drive kinematics: convert (linear, angular) to (left, right) let v = self.last_cmd.linear; let w = self.last_cmd.angular; let left = (v - w * WHEEL_BASE / 2.0) / WHEEL_RADIUS; let right = (v + w * WHEEL_BASE / 2.0) / WHEEL_RADIUS; // Convert rad/s to RPM and clamp to safety limit let to_rpm = 60.0 / (2.0 * std::f32::consts::PI); let left_rpm = (left * to_rpm).clamp(-MAX_RPM, MAX_RPM); let right_rpm = (right * to_rpm).clamp(-MAX_RPM, MAX_RPM); self.wheel_pub.send(WheelCmd { left_rpm, right_rpm }); } fn shutdown(&mut self) -> Result<()> { // SAFETY: zero both motors before exiting — prevents runaway self.wheel_pub.send(WheelCmd { left_rpm: 0.0, right_rpm: 0.0 }); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: DriveNode reads cmd_vel and publishes wheel commands scheduler.add(DriveNode::new()?) .order(0) // only node, runs first .rate(50_u64.hz()) // 50Hz control loop — auto-enables RT .on_miss(Miss::Warn) // log if tick overruns .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 50 Hz [HORUS] Node "Drive" started (Rt, 50 Hz, budget: 16.0ms, deadline: 19.0ms) ^C [HORUS] Shutting down... [HORUS] Node "Drive" shutdown complete ``` ### Key Points - **`#[repr(C)]` + `Copy`** on `WheelCmd` enables zero-copy shared memory (PodTopic path) - **`.rate(50_u64.hz())`** auto-enables RT with 80% budget and 95% deadline - **`shutdown()`** sends zero command — prevents wheels spinning if the program crashes mid-tick - **Kinematics**: `v - w*L/2` and `v + w*L/2` is the standard unicycle-to-differential conversion - **`clamp()`** enforces motor safety limits even if upstream sends dangerous velocities --- ## Recipe: IMU Reader Path: /recipes/imu-reader Description: 100Hz IMU sensor node that reads accelerometer and gyroscope data and publishes orientation estimates. ## IMU Reader Reads IMU hardware at 100Hz, publishes raw `Imu` messages for downstream consumers. Uses a `#[repr(C)]` orientation estimate for zero-copy publishing. ### horus.toml ```toml [package] name = "imu-reader" version = "0.1.0" description = "100Hz IMU sensor with orientation publishing" ``` ### Complete Code ```rust use horus::prelude::*; /// Simplified orientation from integrated gyro #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct Orientation { roll: f32, pitch: f32, yaw: f32, timestamp_ns: u64, } // ── IMU Node ──────────────────────────────────────────────── struct ImuNode { imu_pub: Topic, orientation_pub: Topic, roll: f32, pitch: f32, yaw: f32, tick_count: u64, } impl ImuNode { fn new() -> Result { Ok(Self { imu_pub: Topic::new("imu.raw")?, orientation_pub: Topic::new("imu.orientation")?, roll: 0.0, pitch: 0.0, yaw: 0.0, tick_count: 0, }) } /// Simulate IMU hardware read (replace with real driver) fn read_hardware(&self) -> Imu { Imu { orientation: [1.0, 0.0, 0.0, 0.0], // identity quaternion (w,x,y,z) angular_velocity: [0.01, 0.0, 0.05], // rad/s (roll, pitch, yaw) linear_acceleration: [0.0, 0.0, 9.81], // m/s² (x, y, z) } } } impl Node for ImuNode { fn name(&self) -> &str { "ImuReader" } fn tick(&mut self) { let imu = self.read_hardware(); // Publish raw IMU for any subscriber self.imu_pub.send(imu); // Simple gyro integration (replace with Madgwick/Mahony in production) let dt = 1.0 / 100.0; // 100Hz self.roll += imu.angular_velocity[0] as f32 * dt; self.pitch += imu.angular_velocity[1] as f32 * dt; self.yaw += imu.angular_velocity[2] as f32 * dt; self.tick_count += 1; self.orientation_pub.send(Orientation { roll: self.roll, pitch: self.pitch, yaw: self.yaw, timestamp_ns: self.tick_count * 10_000_000, // 10ms per tick }); } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: IMU reads hardware and publishes scheduler.add(ImuNode::new()?) .order(0) .rate(100_u64.hz()) // 100Hz sensor rate — auto-enables RT .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 100 Hz [HORUS] Node "ImuReader" started (Rt, 100 Hz, budget: 8.0ms, deadline: 9.5ms) ^C [HORUS] Shutting down... [HORUS] Node "ImuReader" shutdown complete ``` ### Key Points - **`Imu`** is a built-in horus message type with `orientation`, `angular_velocity`, `linear_acceleration` - **`#[repr(C)]` + `Copy`** on `Orientation` enables the PodTopic zero-copy path (~50ns latency) - **`read_hardware()`** is a placeholder — replace with your actual I2C/SPI driver call - **No `shutdown()` needed** — sensors don't actuate, so no safety cleanup required - **Gyro integration drifts** — in production, use a Madgwick or complementary filter --- ## Recipe: PID Controller Path: /recipes/pid-controller Description: Generic PID control loop with configurable gains, anti-windup, and derivative filtering. ## PID Controller A reusable PID controller node that reads a setpoint and measured value, then publishes a control output. Includes integral anti-windup and derivative low-pass filtering. ### horus.toml ```toml [package] name = "pid-controller" version = "0.1.0" description = "Generic PID with anti-windup and derivative filtering" ``` ### Complete Code ```rust use horus::prelude::*; /// Setpoint command: what we want #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct Setpoint { target: f32, } /// Measured feedback: what we have #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct Measurement { value: f32, } /// PID output: what to do #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct ControlOutput { command: f32, error: f32, p_term: f32, i_term: f32, d_term: f32, } // ── PID Node ──────────────────────────────────────────────── struct PidNode { setpoint_sub: Topic, measurement_sub: Topic, output_pub: Topic, // PID gains kp: f32, ki: f32, kd: f32, // State integral: f32, prev_error: f32, prev_derivative: f32, // Limits output_min: f32, output_max: f32, integral_max: f32, // Derivative filter coefficient (0.0 = no filter, 0.9 = heavy filter) alpha: f32, // Cached inputs target: f32, measured: f32, } impl PidNode { fn new(kp: f32, ki: f32, kd: f32) -> Result { Ok(Self { setpoint_sub: Topic::new("pid.setpoint")?, measurement_sub: Topic::new("pid.measurement")?, output_pub: Topic::new("pid.output")?, kp, ki, kd, integral: 0.0, prev_error: 0.0, prev_derivative: 0.0, output_min: -1.0, output_max: 1.0, integral_max: 0.5, // anti-windup limit alpha: 0.8, // derivative low-pass filter target: 0.0, measured: 0.0, }) } } impl Node for PidNode { fn name(&self) -> &str { "PID" } fn tick(&mut self) { // IMPORTANT: always recv() every tick to drain buffers if let Some(sp) = self.setpoint_sub.recv() { self.target = sp.target; } if let Some(m) = self.measurement_sub.recv() { self.measured = m.value; } let dt = 1.0 / 200.0; // 200Hz control rate let error = self.target - self.measured; // P term let p_term = self.kp * error; // I term with anti-windup clamping self.integral += error * dt; self.integral = self.integral.clamp(-self.integral_max, self.integral_max); let i_term = self.ki * self.integral; // D term with low-pass filter to reduce noise let raw_derivative = (error - self.prev_error) / dt; let filtered = self.alpha * self.prev_derivative + (1.0 - self.alpha) * raw_derivative; let d_term = self.kd * filtered; self.prev_derivative = filtered; self.prev_error = error; // Total output with saturation let command = (p_term + i_term + d_term).clamp(self.output_min, self.output_max); self.output_pub.send(ControlOutput { command, error, p_term, i_term, d_term, }); } fn shutdown(&mut self) -> Result<()> { // SAFETY: zero the control output on shutdown self.output_pub.send(ControlOutput::default()); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // PID gains: tune for your plant // Execution order: PID reads setpoint + measurement, publishes output scheduler.add(PidNode::new(2.0, 0.5, 0.1)?) .order(0) .rate(200_u64.hz()) // 200Hz control loop — auto-enables RT .budget(400.us()) // 400μs budget (tight for control) .on_miss(Miss::Warn) .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 200 Hz [HORUS] Node "PID" started (Rt, 200 Hz, budget: 400μs, deadline: 4.75ms) ^C [HORUS] Shutting down... [HORUS] Node "PID" shutdown complete ``` ### Key Points - **Anti-windup**: Integral term is clamped to `integral_max` — prevents windup during saturation - **Derivative filter**: Low-pass filter (alpha=0.8) smooths noisy sensor feedback - **`ControlOutput` includes debug fields** (`error`, `p_term`, `i_term`, `d_term`) for tuning - **`shutdown()` zeros output** — prevents actuator from holding last command - **200Hz is typical** for position/velocity PID; use 1kHz+ for current/torque loops - **Gains (kp, ki, kd)** are constructor parameters — wire from config or topic for online tuning --- ## Recipe: LiDAR Obstacle Avoidance Path: /recipes/lidar-obstacle-avoidance Description: Reactive obstacle avoidance using LaserScan data to generate safe velocity commands. ## LiDAR Obstacle Avoidance Reads `LaserScan` data from a 2D LiDAR, identifies obstacles in three zones (left, center, right), and publishes reactive `CmdVel` commands to avoid collisions. Stops if an obstacle is too close. ### horus.toml ```toml [package] name = "lidar-avoidance" version = "0.1.0" description = "Reactive obstacle avoidance from LaserScan" ``` ### Complete Code ```rust use horus::prelude::*; /// Safety zones (meters) const STOP_DISTANCE: f32 = 0.3; // emergency stop const SLOW_DISTANCE: f32 = 0.8; // reduce speed const CRUISE_SPEED: f32 = 0.5; // m/s forward const TURN_SPEED: f32 = 0.8; // rad/s turning // ── Avoidance Node ────────────────────────────────────────── struct AvoidanceNode { scan_sub: Topic, cmd_pub: Topic, } impl AvoidanceNode { fn new() -> Result { Ok(Self { scan_sub: Topic::new("lidar.scan")?, cmd_pub: Topic::new("cmd_vel")?, }) } /// Find minimum range in a slice of the scan fn min_range(ranges: &[f32], start: usize, end: usize) -> f32 { ranges[start..end] .iter() .copied() .filter(|r| r.is_finite() && *r > 0.01) .fold(f32::MAX, f32::min) } } impl Node for AvoidanceNode { fn name(&self) -> &str { "Avoidance" } fn tick(&mut self) { // IMPORTANT: always recv() every tick to drain the buffer let scan = match self.scan_sub.recv() { Some(s) => s, None => return, // no data yet — skip this tick }; let n = scan.ranges.len(); if n == 0 { return; } // Split scan into three zones: left (0..n/3), center (n/3..2n/3), right (2n/3..n) let third = n / 3; let left_min = Self::min_range(&scan.ranges, 0, third); let center_min = Self::min_range(&scan.ranges, third, 2 * third); let right_min = Self::min_range(&scan.ranges, 2 * third, n); // Reactive behavior let cmd = if center_min < STOP_DISTANCE { // WARNING: obstacle dead ahead — emergency stop CmdVel { linear: 0.0, angular: 0.0 } } else if center_min < SLOW_DISTANCE { // Obstacle ahead — turn toward the more open side let angular = if left_min > right_min { TURN_SPEED } else { -TURN_SPEED }; CmdVel { linear: 0.1, angular } } else if left_min < SLOW_DISTANCE { // Obstacle on left — veer right CmdVel { linear: CRUISE_SPEED * 0.7, angular: -TURN_SPEED * 0.5 } } else if right_min < SLOW_DISTANCE { // Obstacle on right — veer left CmdVel { linear: CRUISE_SPEED * 0.7, angular: TURN_SPEED * 0.5 } } else { // Clear — cruise forward CmdVel { linear: CRUISE_SPEED, angular: 0.0 } }; self.cmd_pub.send(cmd); } fn shutdown(&mut self) -> Result<()> { // SAFETY: stop the robot on exit self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0 }); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: reads lidar scan, publishes velocity scheduler.add(AvoidanceNode::new()?) .order(0) .rate(20_u64.hz()) // 20Hz — match typical LiDAR rate .on_miss(Miss::Warn) .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 20 Hz [HORUS] Node "Avoidance" started (Rt, 20 Hz, budget: 40.0ms, deadline: 47.5ms) ^C [HORUS] Shutting down... [HORUS] Node "Avoidance" shutdown complete ``` ### Key Points - **Three-zone split** (left/center/right) is the simplest reactive architecture — extend to N zones for smoother behavior - **`min_range()` filters** invalid readings (`NaN`, `Inf`, near-zero) before comparison - **`STOP_DISTANCE`** is the hard safety limit — tune to your robot's stopping distance at cruise speed - **`shutdown()` sends zero velocity** — robot stops even if killed mid-avoidance-maneuver - **20Hz matches most 2D LiDARs** (RPLiDAR A1/A2, Hokuyo URG) — no benefit running faster than sensor - **Pair with differential-drive recipe** — this publishes `cmd_vel`, the drive recipe subscribes to it --- ## Recipe: Servo Controller Path: /recipes/servo-controller Description: Multi-servo bus controller with position commands, feedback reading, and ordered safe shutdown. ## Servo Controller Controls a bus of servos (e.g., Dynamixel, hobby PWM). Reads position commands from a topic, writes to hardware, publishes joint feedback. Implements ordered shutdown — all servos return to home position before exit. ### horus.toml ```toml [package] name = "servo-controller" version = "0.1.0" description = "Multi-servo bus with safe shutdown" ``` ### Complete Code ```rust use horus::prelude::*; const NUM_SERVOS: usize = 6; const HOME_POSITION: f32 = 0.0; // radians — safe resting position /// Command for all servos #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct ServoGoals { positions: [f32; NUM_SERVOS], // target positions in radians } /// Feedback from all servos #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct ServoFeedback { positions: [f32; NUM_SERVOS], velocities: [f32; NUM_SERVOS], temperatures: [f32; NUM_SERVOS], } // ── Servo Node ────────────────────────────────────────────── struct ServoNode { goal_sub: Topic, feedback_pub: Topic, current_positions: [f32; NUM_SERVOS], } impl ServoNode { fn new() -> Result { Ok(Self { goal_sub: Topic::new("servo.goals")?, feedback_pub: Topic::new("servo.feedback")?, current_positions: [HOME_POSITION; NUM_SERVOS], }) } /// Write positions to hardware bus (replace with real driver) fn write_hardware(&mut self, goals: &[f32; NUM_SERVOS]) { self.current_positions = *goals; } /// Read feedback from hardware bus (replace with real driver) fn read_hardware(&self) -> ServoFeedback { ServoFeedback { positions: self.current_positions, velocities: [0.0; NUM_SERVOS], temperatures: [35.0; NUM_SERVOS], } } } impl Node for ServoNode { fn name(&self) -> &str { "Servo" } fn tick(&mut self) { // IMPORTANT: always recv() every tick to drain the buffer if let Some(goals) = self.goal_sub.recv() { let mut clamped = goals.positions; for pos in &mut clamped { // SAFETY: enforce joint limits to prevent mechanical damage *pos = pos.clamp(-std::f32::consts::PI, std::f32::consts::PI); } self.write_hardware(&clamped); } let feedback = self.read_hardware(); // WARNING: check for overheating servos for (i, temp) in feedback.temperatures.iter().enumerate() { if *temp > 70.0 { eprintln!("WARNING: servo {} temperature {:.1}C exceeds limit", i, temp); } } self.feedback_pub.send(feedback); } fn shutdown(&mut self) -> Result<()> { // SAFETY: return ALL servos to home position before exiting let home = [HOME_POSITION; NUM_SERVOS]; self.write_hardware(&home); self.feedback_pub.send(ServoFeedback { positions: home, velocities: [0.0; NUM_SERVOS], temperatures: [0.0; NUM_SERVOS], }); Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: servo reads goals, writes hardware, publishes feedback scheduler.add(ServoNode::new()?) .order(0) .rate(100_u64.hz()) // 100Hz servo update rate — auto-enables RT .budget(800.us()) // 800μs budget for bus communication .on_miss(Miss::Warn) .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 100 Hz [HORUS] Node "Servo" started (Rt, 100 Hz, budget: 800μs, deadline: 9.5ms) ^C [HORUS] Shutting down... [HORUS] Node "Servo" shutdown complete ``` ### Key Points - **Fixed-size arrays** (`[f32; NUM_SERVOS]`) enable `#[repr(C)]` + `Copy` for zero-copy IPC - **Joint limit clamping** in `tick()` prevents hardware damage regardless of upstream commands - **Temperature monitoring** catches overheating before servo damage - **`shutdown()` returns to home** — critical for robot arms that hold pose under gravity - **800μs budget** accounts for serial bus latency (Dynamixel at 1Mbps takes ~500μs for 6 servos) - **100Hz** is typical for hobby servos; use 200-500Hz for industrial servos --- ## Recipe: Multi-Sensor Fusion Path: /recipes/multi-sensor-fusion Description: Combine IMU and wheel odometry into a fused state estimate using complementary filtering. ## Multi-Sensor Fusion Fuses IMU orientation with wheel odometry position using a complementary filter. Publishes a unified pose estimate. Demonstrates the multi-topic aggregation pattern — cache latest from each sensor, fuse when both available. ### horus.toml ```toml [package] name = "sensor-fusion" version = "0.1.0" description = "IMU + odometry complementary filter" ``` ### Complete Code ```rust use horus::prelude::*; /// Wheel odometry: position from encoder counts #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct WheelOdom { x: f32, y: f32, theta: f32, speed: f32, } /// IMU-derived heading #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct ImuHeading { yaw: f32, yaw_rate: f32, } /// Fused state estimate #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct FusedPose { x: f32, y: f32, theta: f32, speed: f32, confidence: f32, } // ── Fusion Node ───────────────────────────────────────────── struct FusionNode { odom_sub: Topic, imu_sub: Topic, pose_pub: Topic, last_odom: Option, last_imu: Option, alpha: f32, } impl FusionNode { fn new() -> Result { Ok(Self { odom_sub: Topic::new("odom.wheels")?, imu_sub: Topic::new("imu.heading")?, pose_pub: Topic::new("pose.fused")?, last_odom: None, last_imu: None, alpha: 0.7, // favor IMU for heading (less drift than wheels on turns) }) } } impl Node for FusionNode { fn name(&self) -> &str { "Fusion" } fn tick(&mut self) { // IMPORTANT: always recv() ALL topics every tick to drain buffers if let Some(odom) = self.odom_sub.recv() { self.last_odom = Some(odom); } if let Some(imu) = self.imu_sub.recv() { self.last_imu = Some(imu); } // Fuse only when both sources are available let (odom, imu) = match (&self.last_odom, &self.last_imu) { (Some(o), Some(i)) => (o, i), _ => return, }; // Complementary filter: blend odom heading with IMU heading let fused_theta = (1.0 - self.alpha) * odom.theta + self.alpha * imu.yaw; let confidence = if imu.yaw_rate.abs() > 0.5 { 0.6 } else { 0.9 }; self.pose_pub.send(FusedPose { x: odom.x, y: odom.y, theta: fused_theta, speed: odom.speed, confidence, }); } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: fusion reads both sensors and publishes fused pose scheduler.add(FusionNode::new()?) .order(0) .rate(50_u64.hz()) .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 50 Hz [HORUS] Node "Fusion" started (Rt, 50 Hz, budget: 16.0ms, deadline: 19.0ms) ^C [HORUS] Shutting down... [HORUS] Node "Fusion" shutdown complete ``` ### Key Points - **Multi-topic aggregation pattern**: `recv()` all topics, cache with `Option`, fuse when both are `Some` - **Complementary filter** is the simplest sensor fusion — for production, consider an Extended Kalman Filter (EKF) - **`alpha = 0.7`** favors IMU for heading — wheel odometry drifts on carpet/tile; tune per surface - **No `shutdown()` needed** — fusion nodes don't actuate anything - **50Hz output** from 100Hz IMU + 20Hz odometry is fine — fusion runs on cached values - **Confidence field** lets downstream nodes decide how much to trust the estimate --- ## Recipe: Emergency Stop Path: /recipes/emergency-stop Description: Safety-critical E-stop monitor that forces all actuators to safe state when triggered. ## Emergency Stop Monitors an E-stop signal (hardware button, software watchdog, or remote command). When triggered, publishes a zero-velocity command and enters safe state. Uses `Miss::SafeMode` — if the E-stop node itself misses a deadline, the scheduler forces safe state automatically. ### horus.toml ```toml [package] name = "emergency-stop" version = "0.1.0" description = "E-stop monitor with safety state handling" ``` ### Complete Code ```rust use horus::prelude::*; /// E-stop trigger from hardware or software #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct EStopSignal { triggered: u8, // 0 = clear, 1 = triggered (u8 for repr(C) compat) source: u8, // 0 = hardware, 1 = software, 2 = remote } /// Status published by the E-stop monitor #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct SafetyStatus { estop_active: u8, consecutive_clears: u32, uptime_ticks: u64, } // ── E-Stop Monitor ────────────────────────────────────────── struct EStopNode { estop_sub: Topic, cmd_pub: Topic, status_pub: Topic, estop_active: bool, consecutive_clears: u32, ticks: u64, } impl EStopNode { fn new() -> Result { Ok(Self { estop_sub: Topic::new("safety.estop")?, cmd_pub: Topic::new("cmd_vel")?, status_pub: Topic::new("safety.status")?, estop_active: false, consecutive_clears: 0, ticks: 0, }) } } impl Node for EStopNode { fn name(&self) -> &str { "EStop" } fn tick(&mut self) { self.ticks += 1; // IMPORTANT: always recv() every tick if let Some(signal) = self.estop_sub.recv() { if signal.triggered != 0 { // SAFETY: immediately stop all motion self.estop_active = true; self.consecutive_clears = 0; } else { self.consecutive_clears += 1; } } else { // WARNING: no signal received — treat as potential fault // In safety-critical systems, loss of heartbeat = stop self.consecutive_clears = 0; } // Require N consecutive clear signals before releasing E-stop const CLEAR_THRESHOLD: u32 = 50; // 50 ticks at 100Hz = 0.5 seconds if self.estop_active && self.consecutive_clears >= CLEAR_THRESHOLD { self.estop_active = false; } if self.estop_active { // SAFETY: override cmd_vel with zero — stops all motion self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0 }); } self.status_pub.send(SafetyStatus { estop_active: self.estop_active as u8, consecutive_clears: self.consecutive_clears, uptime_ticks: self.ticks, }); } fn shutdown(&mut self) -> Result<()> { // SAFETY: zero velocity on shutdown self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0 }); Ok(()) } fn enter_safe_state(&mut self) { // Called by scheduler if this node misses its deadline self.estop_active = true; self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0 }); } fn is_safe_state(&self) -> bool { self.estop_active } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: E-stop runs LAST — overrides any cmd_vel from other nodes scheduler.add(EStopNode::new()?) .order(100) // high order = runs after drive/planner nodes .rate(100_u64.hz()) // 100Hz safety monitoring — auto-enables RT .budget(200.us()) // tight budget for safety-critical code .deadline(500.us()) // tight deadline .on_miss(Miss::SafeMode) // CRITICAL: if this node misses, force safe state .max_deadline_misses(0) // zero tolerance for misses .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 100 Hz [HORUS] Node "EStop" started (Rt, 100 Hz, budget: 200μs, deadline: 500μs) ^C [HORUS] Shutting down... [HORUS] Node "EStop" shutdown complete ``` ### Key Points - **`enter_safe_state()`** is called by the scheduler if this node misses its deadline — the robot stops automatically - **`Miss::SafeMode`** is the strictest miss policy — any deadline overrun triggers safe state - **`max_deadline_misses(0)`** means zero tolerance — first miss triggers degradation - **High `.order(100)`** ensures E-stop runs AFTER drive/planning nodes — it overrides their `cmd_vel` - **Debounce with `CLEAR_THRESHOLD`** prevents flickering E-stop from bouncing - **No signal = fault** — if the E-stop topic stops publishing, treat it as triggered (fail-safe) - **200μs budget** is generous for this simple node — keeps safety checks deterministic --- ## Recipe: Telemetry Logger Path: /recipes/telemetry-logger Description: Log any topic to CSV file using async I/O — never blocks the control loop. ## Telemetry Logger Subscribes to topics and writes them to a CSV log file using `.async_io()` execution class. File I/O happens on a Tokio blocking thread — it never blocks or delays the real-time control loop. ### horus.toml ```toml [package] name = "telemetry-logger" version = "0.1.0" description = "Non-blocking topic logger to CSV" ``` ### Complete Code ```rust use horus::prelude::*; use std::fs::File; use std::io::Write as IoWrite; /// Pose data to log #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct FusedPose { x: f32, y: f32, theta: f32, speed: f32, confidence: f32, } /// Motor telemetry to log #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct MotorTelemetry { left_rpm: f32, right_rpm: f32, battery_voltage: f32, } // ── Logger Node ───────────────────────────────────────────── struct LoggerNode { pose_sub: Topic, motor_sub: Topic, file: Option, line_count: u64, } impl LoggerNode { fn new() -> Result { Ok(Self { pose_sub: Topic::new("pose.fused")?, motor_sub: Topic::new("motor.telemetry")?, file: None, line_count: 0, }) } } impl Node for LoggerNode { fn name(&self) -> &str { "Logger" } fn init(&mut self) -> Result<()> { // Open log file in init() — runs once before tick loop let mut f = File::create("telemetry.csv") .map_err(|e| Error::Config( format!("Failed to create log file: {}", e) )))?; writeln!(f, "tick,x,y,theta,speed,confidence,left_rpm,right_rpm,battery_v") .map_err(|e| Error::Config( format!("Failed to write header: {}", e) )))?; self.file = Some(f); Ok(()) } fn tick(&mut self) { self.line_count += 1; // IMPORTANT: always recv() every tick to drain buffers let pose = self.pose_sub.recv().unwrap_or_default(); let motor = self.motor_sub.recv().unwrap_or_default(); // Write CSV line — file I/O is safe here because we use async_io() if let Some(ref mut f) = self.file { let _ = writeln!( f, "{},{:.4},{:.4},{:.4},{:.4},{:.2},{:.1},{:.1},{:.2}", self.line_count, pose.x, pose.y, pose.theta, pose.speed, pose.confidence, motor.left_rpm, motor.right_rpm, motor.battery_voltage, ); } } fn shutdown(&mut self) -> Result<()> { // Flush and close the file if let Some(ref mut f) = self.file { let _ = f.flush(); } self.file = None; Ok(()) } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: logger runs on async I/O thread pool — never blocks RT nodes scheduler.add(LoggerNode::new()?) .order(99) // runs after all data-producing nodes .async_io() // Tokio blocking pool — file I/O is safe .rate(10_u64.hz()) // 10Hz logging — enough for post-analysis .build()?; scheduler.run() } ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 10 Hz [HORUS] Node "Logger" started (AsyncIo, 10 Hz) ^C [HORUS] Shutting down... [HORUS] Node "Logger" shutdown complete ``` Generated `telemetry.csv`: ```text tick,x,y,theta,speed,confidence,left_rpm,right_rpm,battery_v 1,0.0000,0.0000,0.0000,0.0000,0.00,0.0,0.0,0.00 2,0.0100,0.0000,0.0100,0.5000,0.90,45.0,47.0,12.40 ... ``` ### Key Points - **`.async_io()`** runs the node on a Tokio blocking thread pool — file writes never block the RT scheduler - **`init()`** opens the file once before the tick loop starts - **`shutdown()`** flushes and closes the file — prevents data loss - **`unwrap_or_default()`** on recv — logger uses default (zeros) if a topic hasn't published yet - **10Hz logging** is typical for post-flight analysis; use 100Hz+ for real-time debugging - **Combine with any other recipe** — just match the topic names and message types --- ## Recipe: Python CV Node Path: /recipes/python-cv-node Description: Python computer vision node using OpenCV with horus.Node for camera processing. ## Python CV Node A Python node that reads camera images, runs OpenCV processing (e.g., ArUco marker detection), and publishes detection results. Uses `horus.Node` with NumPy-backed images for zero-copy interop. ### horus.toml ```toml [package] name = "python-cv" version = "0.1.0" description = "Python computer vision with OpenCV" language = "python" [dependencies] opencv-python = { version = ">=4.8", source = "pypi" } numpy = { version = ">=1.24", source = "pypi" } ``` ### Complete Code ```python import horus import numpy as np # ── Detection result ───────────────────────────────────────── class DetectionResult: """Detected marker with ID and pose.""" def __init__(self, marker_id=0, x=0.0, y=0.0, confidence=0.0): self.marker_id = marker_id self.x = x self.y = y self.confidence = confidence # ── CV Node ────────────────────────────────────────────────── def make_marker_detector(): frame_count = [0] def tick(node): # IMPORTANT: always call recv() every tick to drain the buffer img = node.recv("camera.image") if img is None: return # no frame yet frame_count[0] += 1 # Convert horus Image to NumPy array (zero-copy when possible) frame = np.frombuffer(img.data, dtype=np.uint8).reshape( img.height, img.width, 3 ) # --- OpenCV processing --- # Convert to grayscale for detection gray = frame[:, :, 0] # simplified — use cv2.cvtColor in production # Simulated detection (replace with cv2.aruco.detectMarkers) # In production: # import cv2 # aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) # params = cv2.aruco.DetectorParameters() # corners, ids, rejected = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=params) detection = DetectionResult( marker_id=42, x=float(frame.shape[1] / 2), y=float(frame.shape[0] / 2), confidence=0.95, ) node.send("vision.detections", detection) def shutdown(node): print(f"MarkerDetector: processed {frame_count[0]} frames") return horus.Node(name="MarkerDetector", tick=tick, shutdown=shutdown, subs=["camera.image"], pubs=["vision.detections"], rate=30) # ── Main ───────────────────────────────────────────────────── if __name__ == "__main__": horus.run(make_marker_detector()) ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 30 Hz [HORUS] Node "MarkerDetector" started (30 Hz) ^C [HORUS] Shutting down... MarkerDetector: processed 150 frames [HORUS] Node "MarkerDetector" shutdown complete ``` ### Key Points - **`horus.Node`** wraps the Rust scheduler — Python nodes get the same lifecycle (init/tick/shutdown) - **`self.get("topic")`** is the Python equivalent of `topic.recv()` — always call every tick - **`self.send("topic", data)`** publishes to any topic - **`horus.run(*nodes)`** is the one-liner to start the scheduler - **NumPy zero-copy**: `horus.Image` data can be reshaped into NumPy arrays without copying - **30Hz** matches most USB cameras — no benefit running faster than the sensor - **Pair with Rust nodes**: Python CV node publishes detections, Rust control node subscribes at 100Hz+ --- ## Recipe: ROS2 Bridge Path: /recipes/ros2-bridge Description: Bridge pattern for connecting horus topics to ROS2 topics in a multi-process architecture. ## ROS2 Bridge Bridges horus topics to ROS2 topics using a multi-process architecture. The horus side runs a bridge node that reads from horus shared memory and forwards to a ROS2 process via a shared topic. This enables gradual migration — run horus for real-time control while keeping ROS2 for visualization (RViz) and navigation (Nav2). ### horus.toml ```toml [package] name = "ros2-bridge" version = "0.1.0" description = "Bridge horus topics to ROS2" ``` ### Architecture ```text ┌─────────────────────┐ shared memory ┌─────────────────────┐ │ horus process │ ◄──────────────────────►│ bridge process │ │ │ Topic │ │ │ MotorCtrl (1kHz) │ Topic │ BridgeNode (50Hz) │ │ Planner (10Hz) │ Topic │ → writes to ROS2 │ │ EStop (100Hz) │ │ via DDS │ └─────────────────────┘ └──────────┬──────────┘ │ DDS ┌──────────▼──────────┐ │ ROS2 process │ │ RViz, Nav2, etc. │ └─────────────────────┘ ``` ### Horus-Side Bridge Node ```rust use horus::prelude::*; /// Lightweight bridge payload — serialized for cross-framework transport #[derive(Debug, Clone, Copy, Default)] #[repr(C)] struct BridgePacket { topic_id: u32, // which topic this came from timestamp_ns: u64, linear: f32, // for cmd_vel angular: f32, } // ── Bridge Node ───────────────────────────────────────────── struct BridgeOutNode { cmd_sub: Topic, imu_sub: Topic, bridge_pub: Topic, tick_count: u64, } impl BridgeOutNode { fn new() -> Result { Ok(Self { cmd_sub: Topic::new("cmd_vel")?, imu_sub: Topic::new("imu.raw")?, bridge_pub: Topic::new("bridge.out")?, tick_count: 0, }) } } impl Node for BridgeOutNode { fn name(&self) -> &str { "BridgeOut" } fn tick(&mut self) { self.tick_count += 1; // IMPORTANT: always recv() every tick to drain buffers if let Some(cmd) = self.cmd_sub.recv() { self.bridge_pub.send(BridgePacket { topic_id: 1, // cmd_vel timestamp_ns: self.tick_count * 20_000_000, // 50Hz linear: cmd.linear, angular: cmd.angular, }); } // Drain IMU even if we don't bridge every reading if let Some(_imu) = self.imu_sub.recv() { // Bridge IMU at lower rate if needed } } } fn main() -> Result<()> { let mut scheduler = Scheduler::new(); // Execution order: bridge reads horus topics and publishes bridge packets scheduler.add(BridgeOutNode::new()?) .order(90) // runs after all horus nodes .rate(50_u64.hz()) // 50Hz bridge rate — RViz doesn't need 1kHz .build()?; scheduler.run() } ``` ### ROS2-Side Bridge (Conceptual Python) ```python #!/usr/bin/env python3 """ ROS2 node that reads horus bridge packets and republishes as ROS2 messages. Run this in a separate process with ROS2 sourced. """ import horus # import rclpy # from geometry_msgs.msg import Twist # ros_pub = create_publisher(Twist, '/cmd_vel', 10) def bridge_in_tick(node): # IMPORTANT: always call recv() every tick packet = node.recv("bridge.out") if packet is None: return # Convert horus BridgePacket to ROS2 Twist # twist = Twist() # twist.linear.x = packet.linear # twist.angular.z = packet.angular # ros_pub.publish(twist) pass if __name__ == "__main__": bridge_in = horus.Node(name="BridgeIn", tick=bridge_in_tick, subs=["bridge.out"], rate=50) horus.run(bridge_in) ``` ### Expected Output ```text [HORUS] Scheduler running — tick_rate: 50 Hz [HORUS] Node "BridgeOut" started (Rt, 50 Hz, budget: 16.0ms, deadline: 19.0ms) ^C [HORUS] Shutting down... [HORUS] Node "BridgeOut" shutdown complete ``` ### Key Points - **Multi-process**: horus and ROS2 run in separate processes — horus topics use shared memory across processes automatically - **Rate decimation**: horus control runs at 1kHz, bridge forwards at 50Hz — RViz doesn't need full rate - **`BridgePacket`** is a simplified carrier — in production, serialize full message types - **Gradual migration**: keep ROS2 for visualization/navigation, move real-time control to horus - **No ROS2 dependency in horus**: the bridge node is pure horus — the ROS2 side handles DDS - **Cross-process topics**: `Topic::new("bridge.out")` works across processes via shared memory — no special config needed --- ## Recipe: Coordinate Transform Tree Path: /recipes/transform-frames Description: Set up a robot's coordinate frame tree with base_link, sensors, and end effector frames. # Recipe: Coordinate Transform Tree Build a robot's coordinate frame tree with static sensor mounts and dynamic joint transforms. A `FramePublisher` node registers the frame hierarchy (world -> base_link -> camera_link -> end_effector) and updates dynamic transforms each tick to simulate a moving robot. A `FrameUser` node queries transforms between frames and converts sensor data from camera coordinates into the world frame. ### horus.toml ```toml [package] name = "transform_frames" version = "0.1.0" language = "rust" [dependencies] horus = "0.1" ``` ### Complete Code ```rust use horus::prelude::*; use std::sync::Arc; // ============================================================================ // Node: FramePublisher — registers and updates the coordinate frame tree // ============================================================================ struct FramePublisher { tf: Arc, base_id: Option, ee_id: Option, tick_count: u64, } impl FramePublisher { fn new(tf: Arc) -> Self { Self { tf, base_id: None, ee_id: None, tick_count: 0, } } } impl Node for FramePublisher { fn name(&self) -> &str { "frame_publisher" } fn init(&mut self) -> Result<()> { // Register the frame hierarchy: // // world (root) // └── base_link (dynamic — robot moves in world) // ├── camera_link (static — bolted to chassis) // ├── lidar_link (static — mounted on top) // └── arm_link (dynamic — joint rotates) // └── end_effector (dynamic — tool tip) // Root frame self.tf.add_frame("world").build()?; // Robot base — dynamic, position changes as robot drives self.tf.add_frame("base_link").parent("world").build()?; // Camera — static mount, 10cm forward and 30cm up from base, tilted down 15 degrees self.tf.add_frame("camera_link") .parent("base_link") .static_transform(&Transform::xyz(0.1, 0.0, 0.3).with_rpy(0.0, 0.26, 0.0)) .build()?; // LiDAR — static mount, centered on top of robot, 40cm up self.tf.add_frame("lidar_link") .parent("base_link") .static_transform(&Transform::xyz(0.0, 0.0, 0.4)) .build()?; // Arm link — dynamic, rotates around Z axis self.tf.add_frame("arm_link").parent("base_link").build()?; // End effector — dynamic, extends from arm self.tf.add_frame("end_effector").parent("arm_link").build()?; // Cache frame IDs for fast updates in tick() self.base_id = self.tf.frame_id("base_link"); self.ee_id = self.tf.frame_id("end_effector"); // Print the frame tree for verification self.tf.print_tree(); hlog!(info, "Frame tree registered — {} frames", self.tf.frame_count()); Ok(()) } fn tick(&mut self) { self.tick_count += 1; let t = self.tick_count as f64 * 0.01; // 100Hz -> 10ms per tick let now = timestamp_now(); // Update base_link: robot drives in a circle let radius = 2.0; let speed = 0.2; // rad/s let base_x = radius * (speed * t).cos(); let base_y = radius * (speed * t).sin(); let base_yaw = speed * t + std::f64::consts::FRAC_PI_2; // Face tangent direction let base_tf = Transform::xyz(base_x, base_y, 0.0).with_yaw(base_yaw); if let Some(id) = self.base_id { let _ = self.tf.update_transform_by_id(id, &base_tf, now); } // Update arm_link: joint sweeps back and forth let arm_angle = 0.8 * (t * 0.5).sin(); // +/- 0.8 rad let arm_tf = Transform::xyz(0.15, 0.0, 0.2).with_yaw(arm_angle); let _ = self.tf.update_transform("arm_link", &arm_tf, now); // Update end_effector: extends from arm tip let ee_extension = 0.3; // 30cm arm length let ee_tf = Transform::xyz(ee_extension, 0.0, 0.0); if let Some(id) = self.ee_id { let _ = self.tf.update_transform_by_id(id, &ee_tf, now); } if self.tick_count % 100 == 0 { hlog!(info, "[FRAMES] base=({:.2}, {:.2}), arm_angle={:.2} rad", base_x, base_y, arm_angle); } } fn shutdown(&mut self) -> Result<()> { // SAFETY: Log final frame tree statistics for post-run diagnostics. let stats = self.tf.stats(); hlog!(info, "Frame publisher shutdown — {}", stats.summary()); Ok(()) } } // ============================================================================ // Node: FrameUser — queries transforms and converts sensor data // ============================================================================ struct FrameUser { tf: Arc, detection_pub: Topic<[f64; 3]>, tick_count: u64, } impl FrameUser { fn new(tf: Arc) -> Result> { Ok(Self { tf, detection_pub: Topic::new("detection_world")?, tick_count: 0, }) } } impl Node for FrameUser { fn name(&self) -> &str { "frame_user" } fn init(&mut self) -> Result<()> { hlog!(info, "Frame user ready — will transform detections from camera to world"); Ok(()) } fn tick(&mut self) { self.tick_count += 1; // Simulate a detection at a fixed point in camera frame // (1.5m forward, 0.2m left, 0m up from camera) let detection_in_camera = [1.5, 0.2, 0.0]; // Transform the detection from camera_link to world frame match self.tf.query("camera_link").to("world").point(detection_in_camera) { Ok(world_point) => { self.detection_pub.send(world_point); if self.tick_count % 100 == 0 { hlog!(info, "[USER] Detection in camera: ({:.2}, {:.2}, {:.2}) -> world: ({:.2}, {:.2}, {:.2})", detection_in_camera[0], detection_in_camera[1], detection_in_camera[2], world_point[0], world_point[1], world_point[2] ); } } Err(e) => { if self.tick_count % 500 == 0 { hlog!(warn, "Transform camera->world not available: {}", e); } } } // Query end effector position in world frame if self.tick_count % 100 == 0 { if let Ok(ee_tf) = self.tf.query("end_effector").to("world").lookup() { hlog!(info, "[USER] End effector in world: ({:.2}, {:.2}, {:.2})", ee_tf.translation[0], ee_tf.translation[1], ee_tf.translation[2]); } // Check if any frames are stale (sensor disconnect detection) if self.tf.is_stale_now("base_link", 500_000_000) { // 500ms hlog!(warn, "base_link transform is stale — odometry may be disconnected"); } // Demonstrate the frame chain if let Ok(chain) = self.tf.query("end_effector").to("world").chain() { hlog!(info, "[USER] Frame chain: {}", chain.join(" -> ")); } } } fn shutdown(&mut self) -> Result<()> { // SAFETY: Log the frame tree on shutdown for debugging spatial relationships. hlog!(info, "Frame user shutdown after {} ticks", self.tick_count); self.tf.print_tree(); Ok(()) } } // ============================================================================ // Main — shared TransformFrame instance across nodes // ============================================================================ fn main() -> Result<(), Box> { // Create a shared TransformFrame — both nodes read/write the same tree let tf = Arc::new(TransformFrame::new()); let mut scheduler = Scheduler::new(); // Frame publisher at 100Hz — updates dynamic transforms scheduler.add(FramePublisher::new(Arc::clone(&tf))) .order(0) // NOTE: .rate(100Hz) auto-derives budget=8ms, deadline=9.5ms and marks // this node as real-time. Transform updates must be deterministic so // downstream nodes always see fresh spatial data. .rate(100_u64.hz()) .on_miss(Miss::Skip) .build()?; // Frame user at 50Hz — queries transforms and converts data scheduler.add(FrameUser::new(Arc::clone(&tf))?) .order(1) // NOTE: .rate(50Hz) auto-derives RT scheduling. The user runs at half the // publisher rate, which is typical — transform queries do not need to run // as fast as transform updates. .rate(50_u64.hz()) .on_miss(Miss::Warn) .build()?; hlog!(info, "Transform frame system running — Ctrl+C to stop"); scheduler.build()?.run()?; Ok(()) } ``` ### Expected Output ``` world └── base_link ├── camera_link [static] ├── lidar_link [static] └── arm_link └── end_effector [INFO] Frame tree registered — 6 frames [INFO] Frame user ready — will transform detections from camera to world [INFO] Transform frame system running — Ctrl+C to stop [INFO] [FRAMES] base=(2.00, 0.00), arm_angle=0.00 rad [INFO] [USER] Detection in camera: (1.50, 0.20, 0.00) -> world: (2.07, 0.22, 0.30) [INFO] [USER] End effector in world: (2.43, 0.03, 0.20) [INFO] [USER] Frame chain: end_effector -> arm_link -> base_link -> world [INFO] [FRAMES] base=(1.96, 0.39), arm_angle=0.39 rad [INFO] [USER] Detection in camera: (1.50, 0.20, 0.00) -> world: (1.89, 0.65, 0.30) [INFO] [USER] End effector in world: (2.18, 0.57, 0.20) [INFO] [USER] Frame chain: end_effector -> arm_link -> base_link -> world ^C [INFO] Frame publisher shutdown — 6 total, 2 static, 4 dynamic, depth 4 [INFO] Frame user shutdown after 500 ticks world └── base_link ├── camera_link [static] ├── lidar_link [static] └── arm_link └── end_effector ``` ### Key Points - **`Arc`** shared across nodes: Both the publisher and user hold a reference to the same tree. `TransformFrame` is lock-free internally, so concurrent reads and writes are safe without mutexes. - **Static vs dynamic frames**: `camera_link` and `lidar_link` use `.static_transform()` because they are bolted to the chassis. `base_link`, `arm_link`, and `end_effector` are dynamic and updated every tick. - **`FrameId` caching**: The publisher caches `FrameId` values at init and uses `update_transform_by_id()` in the hot loop. This avoids string-based name resolution (~200ns) and uses the faster ID path (~50ns). - **No `sleep()` calls**: All timing is managed by `.rate()`. The publisher runs at 100Hz and the user at 50Hz. The scheduler handles rate differences. - **Staleness detection**: `is_stale_now()` checks whether a frame's transform data is older than a threshold, which catches disconnected sensors or frozen publishers without polling. ======================================== # SECTION: root ======================================== --- ## Troubleshooting Path: /troubleshooting Description: Fix installation issues, runtime errors, and debug HORUS applications # Troubleshooting HORUS includes utility scripts to help you update, recover from broken installations, and verify your setup. ## Quick Reference | Script | Use When | What It Does | |--------|----------|--------------| | `./install.sh` | Install or update | Full installation from source | | `./uninstall.sh` | Remove HORUS | Complete removal | ## Quick Diagnostic Steps When your HORUS application isn't working: 1. **Check the Monitor**: Run `horus monitor` to see active nodes, topics, and message flow 2. **Examine Logs**: Look for error messages in your terminal output 3. **Verify Topics**: Ensure publisher and subscriber use exact same topic names 4. **Check Shared Memory**: Run `horus clean --shm` to remove stale shared memory regions 5. **Test Individually**: Run nodes one at a time to isolate the problem --- ## Updating HORUS To update to the latest version: ```bash cd /path/to/horus git pull ./install.sh ``` **To preview changes before updating:** ```bash git fetch git log HEAD..@{u} # See what's new git pull ./install.sh ``` **If you have uncommitted changes:** ```bash git stash git pull ./install.sh git stash pop # Restore your changes ``` --- ## Manual Recovery **Use when:** Build errors, corrupted cache, installation broken ### Quick Steps ```bash # Navigate to HORUS source directory cd /path/to/horus # 1. Clean build artifacts cargo clean # 2. Remove cached libraries rm -rf ~/.horus/cache # 3. Fresh install ./install.sh ``` ### When to Use Recovery **Symptoms requiring recovery:** 1. **Build fails:** ``` error: could not compile `horus_core` ``` 2. **Corrupted cache:** ``` error: failed to load source for dependency `horus_core` ``` 3. **Binary doesn't work:** ```bash $ horus --help Segmentation fault ``` 4. **Version mismatches:** ``` error: the package `horus` depends on `horus_core 0.1.0`, but `horus_core 0.1.3` is installed ``` 5. **Broken after system updates:** - Rust updated - System libraries changed - GCC/Clang updated ### What Gets Removed **By `cargo clean`:** - `target/` directory (build artifacts) **By `rm -rf ~/.horus/cache`:** - Installed libraries - Cached dependencies **Never removed (safe):** - `~/.horus/config` (user settings) - `~/.horus/credentials` (registry auth) - Project-local `.horus/` directories - Your source code ### Full Reset (Nuclear Option) If the quick steps don't work, do a complete reset: ```bash # Remove everything HORUS-related cargo clean rm -rf ~/.horus rm -f ~/.cargo/bin/horus # Fresh install ./install.sh ``` --- ## Installation Issues **Problem: "Rust not installed"** ```bash $ ./install.sh Error: Rust is not installed ``` **Solution:** ```bash # Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Then try again ./install.sh ``` --- **Problem: "C compiler not found"** **Solution:** ```bash # Ubuntu/Debian/Raspberry Pi OS - Install ALL required packages sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Fedora/RHEL sudo dnf groupinstall "Development Tools" sudo dnf install -y pkg-config openssl-devel systemd-devel alsa-lib-devel \ libX11-devel libXrandr-devel libXi-devel libXcursor-devel libXinerama-devel \ wayland-devel wayland-protocols-devel libxkbcommon-devel \ vulkan-devel fontconfig-devel freetype-devel \ libv4l-devel ``` --- **Problem: Build fails with linker errors** ``` error: linking with `cc` failed: exit status: 1 error: could not find native static library `X11`, perhaps an -L flag is missing? ``` **Solution:** ```bash # Install ALL missing system libraries (most common cause) # Ubuntu/Debian/Raspberry Pi OS sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Or run manual recovery (see Manual Recovery section) cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- ## Update Issues **Problem: "Build failed" during update** **Solution:** ```bash # Try manual recovery cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- **Problem: "Already up to date" but binary broken** **Solution:** ```bash # Force rebuild ./install.sh ``` --- ## Runtime Issues ### "horus: command not found" **Solution:** ```bash # Add to PATH (add to ~/.bashrc or ~/.zshrc) export PATH="$HOME/.cargo/bin:$PATH" # Then reload shell source ~/.bashrc # or restart terminal # Verify which horus horus --help ``` --- ### Binary exists but doesn't run ```bash $ horus --help Segmentation fault ``` **Solution:** ```bash # Full recovery cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- ### Version mismatch errors ``` error: the package `horus` depends on `horus_core 0.1.0`, but `horus_core 0.1.3` is installed ``` **Why this happens:** - You updated the `horus` CLI to a new version - Your project's `.horus/` directory still has cached dependencies from the old version - The cached `Cargo.lock` references incompatible library versions **Solution (Recommended - Fast & Easy):** ```bash # Clean cached build artifacts and dependencies horus run --clean # This removes .horus/target/ and forces a fresh build # with the new version ``` **Alternative Solutions:** **Option 2: Manual cleanup** ```bash # Remove the entire .horus directory rm -rf .horus/ # Next run will rebuild from scratch horus run ``` **Option 3: Manual recovery (for persistent issues)** ```bash # Only needed if --clean doesn't work # This reinstalls HORUS libraries globally cd /path/to/horus cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` **For multiple projects:** ```bash # Clean all projects in your workspace find ~/your-projects -type d -name ".horus" -exec rm -rf {}/target/ \; ``` --- ### "HORUS source directory not found" (Rust projects) ``` Error: HORUS source directory not found. Please set HORUS_SOURCE environment variable. ``` **Solution:** ```bash # Option 1: Set HORUS_SOURCE (recommended for non-standard installations) export HORUS_SOURCE=/path/to/horus echo 'export HORUS_SOURCE=/path/to/horus' >> ~/.bashrc # Option 2: Install HORUS to a standard location # The CLI checks these paths automatically: # - ~/softmata/horus # - /horus # - /opt/horus # - /usr/local/horus # Verify HORUS source is found horus build ``` **Why this happens:** - `horus run` needs to find HORUS core libraries for Rust compilation - It auto-detects standard installation paths - For custom installations, set `$HORUS_SOURCE` --- ## Topic Creation Errors **Symptom**: Application crashes on startup with: ``` Error: Failed to create `Topic` thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value' ``` **Common Causes:** 1. **Stale Shared Memory from Previous Run** - If your app crashes, shared memory regions can persist **Fix**: Clean shared memory: ```bash horus clean --shm ``` 2. **Insufficient Permissions on Shared Memory** (Linux) **Fix**: Check permissions: ```bash # Fix /dev/shm permissions (if needed) sudo chmod 1777 /dev/shm # Then clean stale regions horus clean --shm ``` 3. **Disk Space Full on `/dev/shm`** **Fix**: Check available space: ```bash df -h /dev/shm ``` 4. **Conflicting Topic Names** - Two Topics with same name but different types **Fix**: Use unique topic names: ```rust // BAD: Same name, different types let topic1: Topic = Topic::new("data")?; let topic2: Topic = Topic::new("data")?; // CONFLICT! // GOOD: Different names let topic1: Topic = Topic::new("sensor_data")?; let topic2: Topic = Topic::new("status_data")?; ``` **General Code Fix:** ```rust // Topic names become file paths on the underlying shared memory system. // Use simple, descriptive names with dots (not slashes): let topic = Topic::new("sensor_data")?; let topic = Topic::new("camera.front.raw")?; ``` --- ### "No such file or directory" when creating Topic **Symptom**: Application crashes with: ``` thread 'main' panicked at 'Failed to create publisher 'camera': No such file or directory' ``` **Cause**: You're using **slashes (`/`)** in your topic name. While slashes work on Linux (parent directories are created automatically), they fail on **macOS** where shared memory uses `shm_open()` which doesn't support embedded slashes. On Linux, topic names map to shared memory files. On macOS, they map to `shm_open()` kernel objects which don't support slashes: ``` Topic: "sensors.camera" → works on all platforms (cross-platform) Topic: "sensors/camera" → works on Linux only, fails on macOS ``` **Fix**: Use dots instead of slashes for cross-platform compatibility: ```rust // NOT RECOMMENDED - fails on macOS let topic: Topic = Topic::new("sensors/camera")?; let topic: Topic = Topic::new("robot/cmd_vel")?; // RECOMMENDED - works on all platforms let topic: Topic = Topic::new("sensors.camera")?; let topic: Topic = Topic::new("robot.cmd_vel")?; ``` **Coming from ROS?** ROS uses slashes (`/sensor/lidar`) because it uses network-based naming. HORUS uses dots because topic names map directly to shared memory file names. See [Topic Naming](/concepts/core-concepts-topic#use-dots-not-slashes) for details. --- ## Topic Not Found / No Messages Received **Symptom**: Subscriber node never receives messages even though publisher is sending. ```rust // recv() always returns None if let Some(data) = self.data_sub.recv() { println!("Got data"); // Never prints } ``` **Common Causes:** 1. **Topic Name Mismatch (Typo)** - This is the #1 cause **Fix**: Verify exact topic names: ```rust // Publisher let pub_topic: Topic = Topic::new("sensor_data")?; // Note: sensor_data // Subscriber (TYPO! Missing underscore) let sub_topic: Topic = Topic::new("sensordata")?; // CORRECT: let sub_topic: Topic = Topic::new("sensor_data")?; // Exact match ``` **Debug with Monitor:** ```bash horus monitor ``` Check the "Topics" section to see active topic names. 2. **Type Mismatch** - Publisher and subscriber use different message types **Fix**: Ensure both use same type: ```rust // Publisher let pub_topic: Topic = Topic::new("data")?; pub_topic.send(3.14); // Subscriber (WRONG TYPE) let sub_topic: Topic = Topic::new("data")?; // f64 != f32 // CORRECT: let sub_topic: Topic = Topic::new("data")?; // Same type ``` 3. **Publisher Hasn't Sent Yet** - Subscriber starts before publisher sends first message - This is normal! First `recv()` will return `None` **Fix**: Check multiple ticks: ```rust impl Node for SubscriberNode { fn tick(&mut self) { if let Some(msg) = self.topic.recv() { // Process message } else { // No message yet - this is OK on first few ticks } } } ``` 4. **Wrong Priority Order** - Subscriber runs before publisher in same tick **Fix**: Set priorities correctly: ```rust // Publisher should run first (lower order number) scheduler.add(PublisherNode::new()?).order(0).build()?; // Subscriber runs after (higher order number) scheduler.add(SubscriberNode::new()?).order(1).build()?; ``` --- ## Application Hangs / Deadlock **Symptom**: Your app starts but freezes with no error messages. ``` Starting application... [Nodes initialized] [Application freezes - no output] ``` **Common Causes:** 1. **Infinite Loop in `tick()`** ```rust // BAD: Never returns! fn tick(&mut self) { loop { // Process data } } // GOOD: Tick returns after work fn tick(&mut self) { self.process_data(); // Return naturally — scheduler calls tick() again next frame } ``` 2. **Blocking Operations in `tick()`** ```rust // BAD: Blocks scheduler fn tick(&mut self) { std::thread::sleep(Duration::from_secs(10)); // Blocks everything! } // GOOD: Use tick counter for delays fn tick(&mut self) { self.tick_count += 1; // Execute every 10 ticks (~167ms at 60 FPS) if self.tick_count % 10 == 0 { self.slow_operation(); } } ``` 3. **Waiting Forever for Messages** ```rust // BAD: Blocking wait fn tick(&mut self) { while self.data_sub.recv().is_none() { // Infinite loop if no messages! } } // GOOD: Non-blocking receive fn tick(&mut self) { if let Some(data) = self.data_sub.recv() { // Process data } // Continue even if no message } ``` 4. **Circular Priority Dependencies** - Node A waits for Node B, Node B waits for Node A **Fix**: Ensure data flows one direction: ```rust // BAD: Circular dependency // Node A (priority 0) subscribes to "data_b" // Node B (priority 1) subscribes to "data_a" // Both wait for each other! // GOOD: Unidirectional flow // Node A (priority 0) publishes to "data_a" // Node B (priority 1) subscribes to "data_a", publishes to "data_b" // Node C (priority 2) subscribes to "data_b" ``` 5. **Debug with Logging** ```rust fn tick(&mut self) { hlog!(debug, "Tick started"); // Your code here hlog!(debug, "Tick completed"); } ``` If you see "Tick started" but never "Tick completed", the hang is in your code. --- ## Messages Silently Dropped **Symptom**: Publisher sends messages but subscriber never receives them, and no error is reported. **Cause**: For non-POD (serialized) messages, the serialized data exceeds the slot size (default 8KB). The `send()` method is lossy — it retries briefly then drops the message, incrementing an internal failure counter. **Fix**: **1. Check Message Size:** ```rust use std::mem::size_of; // POD messages always fit (slot = size_of::()) // Non-POD messages must serialize within the slot size (default 8KB) println!("Message size: {} bytes", size_of::()); ``` **2. Keep Messages Reasonably Sized:** ```rust // BAD: Variable size — may exceed limit #[derive(Clone, Serialize, Deserialize)] pub struct LargeMessage { pub data: Vec, } // GOOD: Fixed size #[derive(Clone, Serialize, Deserialize)] pub struct LargeMessage { pub data: [u8; 4096], // Fixed 4KB } // BETTER: Split into multiple messages #[derive(Clone, Serialize, Deserialize)] pub struct MessageChunk { pub chunk_id: u32, pub total_chunks: u32, pub data: [u8; 1024], } ``` **3. Use Monitor to Check:** ```bash # The monitor shows send failure counts per topic horus monitor ``` --- ## Build and Compilation Issues ### "unresolved import" or "cannot find type in this scope" **Symptom**: Code won't compile, missing types or functions. **Fix**: Ensure HORUS is in your `Cargo.toml` dependencies: ```toml [dependencies] horus = { path = "..." } horus_library = { path = "..." } # For standard messages (CmdVel, Twist, etc.) ``` Import the prelude: ```rust use horus::prelude::*; // Provides Twist, LaserScan, CmdVel, etc. ``` ### "trait bound ... is not satisfied" **Symptom**: Compiler says your message doesn't implement required traits. **Fix**: Add required derives: ```rust // Add these three derives to all messages #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MyMessage { pub field: f32, } ``` --- ## Performance Issues **Problem: Slow builds** **Solution:** ```bash # Use release mode (optimized) horus run --release ``` --- **Problem: Large disk usage** **Solution:** ```bash # Clean old cargo cache cargo clean # Remove unused dependencies cargo install cargo-cache cargo cache --autoclean ``` --- **Problem: Large `.horus/target/` directory (Rust projects)** **Why this happens:** - Cargo stores build artifacts in `.horus/target/` - Debug builds are unoptimized and larger - Incremental compilation caches intermediate files **Solution:** ```bash # Clean build artifacts in current project rm -rf .horus/target/ # Or use horus clean flag (next build will be slower) horus run --clean # Regular cleanup (if working on multiple projects) find . -type d -name ".horus" -exec rm -rf {}/target/ \; # Add to .gitignore (already included in horus new templates) echo ".horus/target/" >> .gitignore ``` **Disk usage typical sizes:** - `.horus/target/debug/`: ~10-100 MB (incremental builds) - `.horus/target/release/`: ~5-50 MB (optimized, no debug symbols) **Best practices:** - `.horus/` is in `.gitignore` by default - Clean periodically if disk space is limited --- ## Using the Monitor to Debug The monitor is your best debugging tool for runtime issues. **Starting the Monitor:** ```bash # Terminal 1: Run your application horus run # Terminal 2: Start monitor horus monitor ``` **Monitor Features:** **1. Nodes Tab:** - Shows all running nodes - Displays node state (Running, Error, Stopped) - Shows tick count and timing - Highlights nodes that aren't ticking (stuck) **2. Topics Tab:** - Lists all active topics - Shows message types - Displays publisher/subscriber counts - **0 publishers** = no one is sending - **0 subscribers** = no one is listening **3. Metrics Tab:** - **IPC Latency**: Communication time (should be <1µs) - **Tick Duration**: How long each node takes - **Message Counts**: Total sent/received - If sent > 0 but received = 0, subscriber issue - If sent = 0, publisher issue **4. Graph Tab:** - Visual node graph - Shows message flow between nodes - Disconnected nodes = topic mismatch **Debug Workflow:** ``` 1. Check Nodes tab -> All nodes Running? (If Error, check logs) 2. Check Topics tab -> Topics exist? (If no, topic name typo) -> Publishers > 0? (If no, publisher not working) -> Subscribers > 0? (If no, subscriber not created) 3. Check Metrics tab -> Messages sent > 0? (If no, publisher not sending) -> Messages received > 0? (If no, subscriber not receiving) -> IPC latency sane? (If >1ms, system issue) 4. Check Graph tab -> Nodes connected? (If no, topic name mismatch) ``` **Example Debug Session:** ```bash # Problem: Subscriber not receiving messages # Monitor shows: # Nodes: SensorNode (Running), DisplayNode (Running) # Topics: "sensor_data" (1 pub, 0 sub) <-- AHA! # Issue: No subscribers! # Fix: Check DisplayNode - likely wrong topic name ``` --- ## Reading Log Output ### Log Levels HORUS nodes can log at different severity levels: ```rust fn tick(&mut self) { hlog!(debug, "Detailed info for debugging"); hlog!(info, "Normal informational message"); hlog!(warn, "Something unusual happened"); hlog!(error, "Something went wrong!"); } ``` ### Log Format Console output uses ANSI-colored formatting: ``` [INFO] [SensorNode] Sensor initialized │ │ │ │ │ └─ Message │ └─ Node name └─ Log level (INFO, WARN, ERROR, DEBUG) ``` Timestamps are included in the shared memory log buffer (visible in the monitor), formatted as `HH:MM:SS.mmm`. --- ## Common Patterns and Anti-Patterns ### [OK] DO: Check recv() for None ```rust fn tick(&mut self) { if let Some(msg) = self.topic.recv() { // Process message } // No message? That's OK, just continue } ``` ### [FAIL] DON'T: Unwrap recv() ```rust fn tick(&mut self) { let msg = self.topic.recv().unwrap(); // PANIC if no message! } ``` ### [OK] DO: Use Result for errors ```rust impl Node for MyNode { fn init(&mut self) -> Result<()> { if self.sensor.is_broken() { return Err(Error::node("MyNode", "Sensor initialization failed")); } Ok(()) } } ``` ### [FAIL] DON'T: panic!() in nodes ```rust fn init(&mut self) -> Result<()> { if self.sensor.is_broken() { panic!("Sensor broken"); // DON'T DO THIS } Ok(()) } ``` ### [OK] DO: Keep tick() fast ```rust fn tick(&mut self) { // Quick operations only let data = self.sensor.read_cached(); self.topic.send(data); } ``` ### [FAIL] DON'T: Block in tick() ```rust fn tick(&mut self) { thread::sleep(Duration::from_millis(100)); // Blocks everything! let data = self.network.fetch(); // Network I/O blocks! } ``` --- ## Best Practices ### Regular Maintenance **Weekly (active development):** ```bash git pull && ./install.sh # Pulls latest and rebuilds ``` **After system updates:** ```bash # If Rust/GCC updated, run manual recovery cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` ### CI/CD Integration ```bash # In CI pipeline ./install.sh || (cargo clean && rm -rf ~/.horus/cache && ./install.sh) ``` ### Debugging Workflow 1. **First: Check horus works** ```bash horus --help ``` 2. **If issues: Update** ```bash git pull && ./install.sh ``` 3. **If errors: Manual recovery** ```bash cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- ## Getting Help If you're still having issues: 1. **Try manual recovery:** ```bash cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` 2. **Add Debug Logging:** ```rust // Add hlog!(debug, ...) in your nodes to trace execution hlog!(debug, "Node state: {:?}", self.state); ``` 3. **Test with Minimal Example:** - Strip down to simplest possible code - Add complexity back one piece at a time - Identify what causes the error 4. **Check System Resources:** ```bash # Check available shared memory (Linux) df -h /dev/shm # Clean stale shared memory if needed horus clean --shm ``` 5. **Report the issue:** - GitHub: https://github.com/softmata/horus/issues - Include: full error message, minimal code example, OS and platform --- ## Next Steps - **[Installation](/getting-started/installation)** - First-time installation guide - **[CLI Reference](/development/cli-reference)** - All horus commands - **[Examples](/rust/examples/basic-examples)** - Working code examples - **[Performance](/performance/performance)** - Optimization tips - **[Testing](/development/testing)** - Test your nodes to prevent runtime errors ======================================== # SECTION: Other ======================================== --- ## Troubleshooting Path: /troubleshooting Description: Fix installation issues, runtime errors, and debug HORUS applications # Troubleshooting HORUS includes utility scripts to help you update, recover from broken installations, and verify your setup. ## Quick Reference | Script | Use When | What It Does | |--------|----------|--------------| | `./install.sh` | Install or update | Full installation from source | | `./uninstall.sh` | Remove HORUS | Complete removal | ## Quick Diagnostic Steps When your HORUS application isn't working: 1. **Check the Monitor**: Run `horus monitor` to see active nodes, topics, and message flow 2. **Examine Logs**: Look for error messages in your terminal output 3. **Verify Topics**: Ensure publisher and subscriber use exact same topic names 4. **Check Shared Memory**: Run `horus clean --shm` to remove stale shared memory regions 5. **Test Individually**: Run nodes one at a time to isolate the problem --- ## Updating HORUS To update to the latest version: ```bash cd /path/to/horus git pull ./install.sh ``` **To preview changes before updating:** ```bash git fetch git log HEAD..@{u} # See what's new git pull ./install.sh ``` **If you have uncommitted changes:** ```bash git stash git pull ./install.sh git stash pop # Restore your changes ``` --- ## Manual Recovery **Use when:** Build errors, corrupted cache, installation broken ### Quick Steps ```bash # Navigate to HORUS source directory cd /path/to/horus # 1. Clean build artifacts cargo clean # 2. Remove cached libraries rm -rf ~/.horus/cache # 3. Fresh install ./install.sh ``` ### When to Use Recovery **Symptoms requiring recovery:** 1. **Build fails:** ``` error: could not compile `horus_core` ``` 2. **Corrupted cache:** ``` error: failed to load source for dependency `horus_core` ``` 3. **Binary doesn't work:** ```bash $ horus --help Segmentation fault ``` 4. **Version mismatches:** ``` error: the package `horus` depends on `horus_core 0.1.0`, but `horus_core 0.1.3` is installed ``` 5. **Broken after system updates:** - Rust updated - System libraries changed - GCC/Clang updated ### What Gets Removed **By `cargo clean`:** - `target/` directory (build artifacts) **By `rm -rf ~/.horus/cache`:** - Installed libraries - Cached dependencies **Never removed (safe):** - `~/.horus/config` (user settings) - `~/.horus/credentials` (registry auth) - Project-local `.horus/` directories - Your source code ### Full Reset (Nuclear Option) If the quick steps don't work, do a complete reset: ```bash # Remove everything HORUS-related cargo clean rm -rf ~/.horus rm -f ~/.cargo/bin/horus # Fresh install ./install.sh ``` --- ## Installation Issues **Problem: "Rust not installed"** ```bash $ ./install.sh Error: Rust is not installed ``` **Solution:** ```bash # Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Then try again ./install.sh ``` --- **Problem: "C compiler not found"** **Solution:** ```bash # Ubuntu/Debian/Raspberry Pi OS - Install ALL required packages sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Fedora/RHEL sudo dnf groupinstall "Development Tools" sudo dnf install -y pkg-config openssl-devel systemd-devel alsa-lib-devel \ libX11-devel libXrandr-devel libXi-devel libXcursor-devel libXinerama-devel \ wayland-devel wayland-protocols-devel libxkbcommon-devel \ vulkan-devel fontconfig-devel freetype-devel \ libv4l-devel ``` --- **Problem: Build fails with linker errors** ``` error: linking with `cc` failed: exit status: 1 error: could not find native static library `X11`, perhaps an -L flag is missing? ``` **Solution:** ```bash # Install ALL missing system libraries (most common cause) # Ubuntu/Debian/Raspberry Pi OS sudo apt update sudo apt install -y build-essential pkg-config \ libssl-dev libudev-dev libasound2-dev \ libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libvulkan-dev libfontconfig-dev libfreetype-dev \ libv4l-dev # Or run manual recovery (see Manual Recovery section) cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- ## Update Issues **Problem: "Build failed" during update** **Solution:** ```bash # Try manual recovery cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- **Problem: "Already up to date" but binary broken** **Solution:** ```bash # Force rebuild ./install.sh ``` --- ## Runtime Issues ### "horus: command not found" **Solution:** ```bash # Add to PATH (add to ~/.bashrc or ~/.zshrc) export PATH="$HOME/.cargo/bin:$PATH" # Then reload shell source ~/.bashrc # or restart terminal # Verify which horus horus --help ``` --- ### Binary exists but doesn't run ```bash $ horus --help Segmentation fault ``` **Solution:** ```bash # Full recovery cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- ### Version mismatch errors ``` error: the package `horus` depends on `horus_core 0.1.0`, but `horus_core 0.1.3` is installed ``` **Why this happens:** - You updated the `horus` CLI to a new version - Your project's `.horus/` directory still has cached dependencies from the old version - The cached `Cargo.lock` references incompatible library versions **Solution (Recommended - Fast & Easy):** ```bash # Clean cached build artifacts and dependencies horus run --clean # This removes .horus/target/ and forces a fresh build # with the new version ``` **Alternative Solutions:** **Option 2: Manual cleanup** ```bash # Remove the entire .horus directory rm -rf .horus/ # Next run will rebuild from scratch horus run ``` **Option 3: Manual recovery (for persistent issues)** ```bash # Only needed if --clean doesn't work # This reinstalls HORUS libraries globally cd /path/to/horus cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` **For multiple projects:** ```bash # Clean all projects in your workspace find ~/your-projects -type d -name ".horus" -exec rm -rf {}/target/ \; ``` --- ### "HORUS source directory not found" (Rust projects) ``` Error: HORUS source directory not found. Please set HORUS_SOURCE environment variable. ``` **Solution:** ```bash # Option 1: Set HORUS_SOURCE (recommended for non-standard installations) export HORUS_SOURCE=/path/to/horus echo 'export HORUS_SOURCE=/path/to/horus' >> ~/.bashrc # Option 2: Install HORUS to a standard location # The CLI checks these paths automatically: # - ~/softmata/horus # - /horus # - /opt/horus # - /usr/local/horus # Verify HORUS source is found horus build ``` **Why this happens:** - `horus run` needs to find HORUS core libraries for Rust compilation - It auto-detects standard installation paths - For custom installations, set `$HORUS_SOURCE` --- ## Topic Creation Errors **Symptom**: Application crashes on startup with: ``` Error: Failed to create `Topic` thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value' ``` **Common Causes:** 1. **Stale Shared Memory from Previous Run** - If your app crashes, shared memory regions can persist **Fix**: Clean shared memory: ```bash horus clean --shm ``` 2. **Insufficient Permissions on Shared Memory** (Linux) **Fix**: Check permissions: ```bash # Fix /dev/shm permissions (if needed) sudo chmod 1777 /dev/shm # Then clean stale regions horus clean --shm ``` 3. **Disk Space Full on `/dev/shm`** **Fix**: Check available space: ```bash df -h /dev/shm ``` 4. **Conflicting Topic Names** - Two Topics with same name but different types **Fix**: Use unique topic names: ```rust // BAD: Same name, different types let topic1: Topic = Topic::new("data")?; let topic2: Topic = Topic::new("data")?; // CONFLICT! // GOOD: Different names let topic1: Topic = Topic::new("sensor_data")?; let topic2: Topic = Topic::new("status_data")?; ``` **General Code Fix:** ```rust // Topic names become file paths on the underlying shared memory system. // Use simple, descriptive names with dots (not slashes): let topic = Topic::new("sensor_data")?; let topic = Topic::new("camera.front.raw")?; ``` --- ### "No such file or directory" when creating Topic **Symptom**: Application crashes with: ``` thread 'main' panicked at 'Failed to create publisher 'camera': No such file or directory' ``` **Cause**: You're using **slashes (`/`)** in your topic name. While slashes work on Linux (parent directories are created automatically), they fail on **macOS** where shared memory uses `shm_open()` which doesn't support embedded slashes. On Linux, topic names map to shared memory files. On macOS, they map to `shm_open()` kernel objects which don't support slashes: ``` Topic: "sensors.camera" → works on all platforms (cross-platform) Topic: "sensors/camera" → works on Linux only, fails on macOS ``` **Fix**: Use dots instead of slashes for cross-platform compatibility: ```rust // NOT RECOMMENDED - fails on macOS let topic: Topic = Topic::new("sensors/camera")?; let topic: Topic = Topic::new("robot/cmd_vel")?; // RECOMMENDED - works on all platforms let topic: Topic = Topic::new("sensors.camera")?; let topic: Topic = Topic::new("robot.cmd_vel")?; ``` **Coming from ROS?** ROS uses slashes (`/sensor/lidar`) because it uses network-based naming. HORUS uses dots because topic names map directly to shared memory file names. See [Topic Naming](/concepts/core-concepts-topic#use-dots-not-slashes) for details. --- ## Topic Not Found / No Messages Received **Symptom**: Subscriber node never receives messages even though publisher is sending. ```rust // recv() always returns None if let Some(data) = self.data_sub.recv() { println!("Got data"); // Never prints } ``` **Common Causes:** 1. **Topic Name Mismatch (Typo)** - This is the #1 cause **Fix**: Verify exact topic names: ```rust // Publisher let pub_topic: Topic = Topic::new("sensor_data")?; // Note: sensor_data // Subscriber (TYPO! Missing underscore) let sub_topic: Topic = Topic::new("sensordata")?; // CORRECT: let sub_topic: Topic = Topic::new("sensor_data")?; // Exact match ``` **Debug with Monitor:** ```bash horus monitor ``` Check the "Topics" section to see active topic names. 2. **Type Mismatch** - Publisher and subscriber use different message types **Fix**: Ensure both use same type: ```rust // Publisher let pub_topic: Topic = Topic::new("data")?; pub_topic.send(3.14); // Subscriber (WRONG TYPE) let sub_topic: Topic = Topic::new("data")?; // f64 != f32 // CORRECT: let sub_topic: Topic = Topic::new("data")?; // Same type ``` 3. **Publisher Hasn't Sent Yet** - Subscriber starts before publisher sends first message - This is normal! First `recv()` will return `None` **Fix**: Check multiple ticks: ```rust impl Node for SubscriberNode { fn tick(&mut self) { if let Some(msg) = self.topic.recv() { // Process message } else { // No message yet - this is OK on first few ticks } } } ``` 4. **Wrong Priority Order** - Subscriber runs before publisher in same tick **Fix**: Set priorities correctly: ```rust // Publisher should run first (lower order number) scheduler.add(PublisherNode::new()?).order(0).build()?; // Subscriber runs after (higher order number) scheduler.add(SubscriberNode::new()?).order(1).build()?; ``` --- ## Application Hangs / Deadlock **Symptom**: Your app starts but freezes with no error messages. ``` Starting application... [Nodes initialized] [Application freezes - no output] ``` **Common Causes:** 1. **Infinite Loop in `tick()`** ```rust // BAD: Never returns! fn tick(&mut self) { loop { // Process data } } // GOOD: Tick returns after work fn tick(&mut self) { self.process_data(); // Return naturally — scheduler calls tick() again next frame } ``` 2. **Blocking Operations in `tick()`** ```rust // BAD: Blocks scheduler fn tick(&mut self) { std::thread::sleep(Duration::from_secs(10)); // Blocks everything! } // GOOD: Use tick counter for delays fn tick(&mut self) { self.tick_count += 1; // Execute every 10 ticks (~167ms at 60 FPS) if self.tick_count % 10 == 0 { self.slow_operation(); } } ``` 3. **Waiting Forever for Messages** ```rust // BAD: Blocking wait fn tick(&mut self) { while self.data_sub.recv().is_none() { // Infinite loop if no messages! } } // GOOD: Non-blocking receive fn tick(&mut self) { if let Some(data) = self.data_sub.recv() { // Process data } // Continue even if no message } ``` 4. **Circular Priority Dependencies** - Node A waits for Node B, Node B waits for Node A **Fix**: Ensure data flows one direction: ```rust // BAD: Circular dependency // Node A (priority 0) subscribes to "data_b" // Node B (priority 1) subscribes to "data_a" // Both wait for each other! // GOOD: Unidirectional flow // Node A (priority 0) publishes to "data_a" // Node B (priority 1) subscribes to "data_a", publishes to "data_b" // Node C (priority 2) subscribes to "data_b" ``` 5. **Debug with Logging** ```rust fn tick(&mut self) { hlog!(debug, "Tick started"); // Your code here hlog!(debug, "Tick completed"); } ``` If you see "Tick started" but never "Tick completed", the hang is in your code. --- ## Messages Silently Dropped **Symptom**: Publisher sends messages but subscriber never receives them, and no error is reported. **Cause**: For non-POD (serialized) messages, the serialized data exceeds the slot size (default 8KB). The `send()` method is lossy — it retries briefly then drops the message, incrementing an internal failure counter. **Fix**: **1. Check Message Size:** ```rust use std::mem::size_of; // POD messages always fit (slot = size_of::()) // Non-POD messages must serialize within the slot size (default 8KB) println!("Message size: {} bytes", size_of::()); ``` **2. Keep Messages Reasonably Sized:** ```rust // BAD: Variable size — may exceed limit #[derive(Clone, Serialize, Deserialize)] pub struct LargeMessage { pub data: Vec, } // GOOD: Fixed size #[derive(Clone, Serialize, Deserialize)] pub struct LargeMessage { pub data: [u8; 4096], // Fixed 4KB } // BETTER: Split into multiple messages #[derive(Clone, Serialize, Deserialize)] pub struct MessageChunk { pub chunk_id: u32, pub total_chunks: u32, pub data: [u8; 1024], } ``` **3. Use Monitor to Check:** ```bash # The monitor shows send failure counts per topic horus monitor ``` --- ## Build and Compilation Issues ### "unresolved import" or "cannot find type in this scope" **Symptom**: Code won't compile, missing types or functions. **Fix**: Ensure HORUS is in your `Cargo.toml` dependencies: ```toml [dependencies] horus = { path = "..." } horus_library = { path = "..." } # For standard messages (CmdVel, Twist, etc.) ``` Import the prelude: ```rust use horus::prelude::*; // Provides Twist, LaserScan, CmdVel, etc. ``` ### "trait bound ... is not satisfied" **Symptom**: Compiler says your message doesn't implement required traits. **Fix**: Add required derives: ```rust // Add these three derives to all messages #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MyMessage { pub field: f32, } ``` --- ## Performance Issues **Problem: Slow builds** **Solution:** ```bash # Use release mode (optimized) horus run --release ``` --- **Problem: Large disk usage** **Solution:** ```bash # Clean old cargo cache cargo clean # Remove unused dependencies cargo install cargo-cache cargo cache --autoclean ``` --- **Problem: Large `.horus/target/` directory (Rust projects)** **Why this happens:** - Cargo stores build artifacts in `.horus/target/` - Debug builds are unoptimized and larger - Incremental compilation caches intermediate files **Solution:** ```bash # Clean build artifacts in current project rm -rf .horus/target/ # Or use horus clean flag (next build will be slower) horus run --clean # Regular cleanup (if working on multiple projects) find . -type d -name ".horus" -exec rm -rf {}/target/ \; # Add to .gitignore (already included in horus new templates) echo ".horus/target/" >> .gitignore ``` **Disk usage typical sizes:** - `.horus/target/debug/`: ~10-100 MB (incremental builds) - `.horus/target/release/`: ~5-50 MB (optimized, no debug symbols) **Best practices:** - `.horus/` is in `.gitignore` by default - Clean periodically if disk space is limited --- ## Using the Monitor to Debug The monitor is your best debugging tool for runtime issues. **Starting the Monitor:** ```bash # Terminal 1: Run your application horus run # Terminal 2: Start monitor horus monitor ``` **Monitor Features:** **1. Nodes Tab:** - Shows all running nodes - Displays node state (Running, Error, Stopped) - Shows tick count and timing - Highlights nodes that aren't ticking (stuck) **2. Topics Tab:** - Lists all active topics - Shows message types - Displays publisher/subscriber counts - **0 publishers** = no one is sending - **0 subscribers** = no one is listening **3. Metrics Tab:** - **IPC Latency**: Communication time (should be <1µs) - **Tick Duration**: How long each node takes - **Message Counts**: Total sent/received - If sent > 0 but received = 0, subscriber issue - If sent = 0, publisher issue **4. Graph Tab:** - Visual node graph - Shows message flow between nodes - Disconnected nodes = topic mismatch **Debug Workflow:** ``` 1. Check Nodes tab -> All nodes Running? (If Error, check logs) 2. Check Topics tab -> Topics exist? (If no, topic name typo) -> Publishers > 0? (If no, publisher not working) -> Subscribers > 0? (If no, subscriber not created) 3. Check Metrics tab -> Messages sent > 0? (If no, publisher not sending) -> Messages received > 0? (If no, subscriber not receiving) -> IPC latency sane? (If >1ms, system issue) 4. Check Graph tab -> Nodes connected? (If no, topic name mismatch) ``` **Example Debug Session:** ```bash # Problem: Subscriber not receiving messages # Monitor shows: # Nodes: SensorNode (Running), DisplayNode (Running) # Topics: "sensor_data" (1 pub, 0 sub) <-- AHA! # Issue: No subscribers! # Fix: Check DisplayNode - likely wrong topic name ``` --- ## Reading Log Output ### Log Levels HORUS nodes can log at different severity levels: ```rust fn tick(&mut self) { hlog!(debug, "Detailed info for debugging"); hlog!(info, "Normal informational message"); hlog!(warn, "Something unusual happened"); hlog!(error, "Something went wrong!"); } ``` ### Log Format Console output uses ANSI-colored formatting: ``` [INFO] [SensorNode] Sensor initialized │ │ │ │ │ └─ Message │ └─ Node name └─ Log level (INFO, WARN, ERROR, DEBUG) ``` Timestamps are included in the shared memory log buffer (visible in the monitor), formatted as `HH:MM:SS.mmm`. --- ## Common Patterns and Anti-Patterns ### [OK] DO: Check recv() for None ```rust fn tick(&mut self) { if let Some(msg) = self.topic.recv() { // Process message } // No message? That's OK, just continue } ``` ### [FAIL] DON'T: Unwrap recv() ```rust fn tick(&mut self) { let msg = self.topic.recv().unwrap(); // PANIC if no message! } ``` ### [OK] DO: Use Result for errors ```rust impl Node for MyNode { fn init(&mut self) -> Result<()> { if self.sensor.is_broken() { return Err(Error::node("MyNode", "Sensor initialization failed")); } Ok(()) } } ``` ### [FAIL] DON'T: panic!() in nodes ```rust fn init(&mut self) -> Result<()> { if self.sensor.is_broken() { panic!("Sensor broken"); // DON'T DO THIS } Ok(()) } ``` ### [OK] DO: Keep tick() fast ```rust fn tick(&mut self) { // Quick operations only let data = self.sensor.read_cached(); self.topic.send(data); } ``` ### [FAIL] DON'T: Block in tick() ```rust fn tick(&mut self) { thread::sleep(Duration::from_millis(100)); // Blocks everything! let data = self.network.fetch(); // Network I/O blocks! } ``` --- ## Best Practices ### Regular Maintenance **Weekly (active development):** ```bash git pull && ./install.sh # Pulls latest and rebuilds ``` **After system updates:** ```bash # If Rust/GCC updated, run manual recovery cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` ### CI/CD Integration ```bash # In CI pipeline ./install.sh || (cargo clean && rm -rf ~/.horus/cache && ./install.sh) ``` ### Debugging Workflow 1. **First: Check horus works** ```bash horus --help ``` 2. **If issues: Update** ```bash git pull && ./install.sh ``` 3. **If errors: Manual recovery** ```bash cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` --- ## Getting Help If you're still having issues: 1. **Try manual recovery:** ```bash cargo clean && rm -rf ~/.horus/cache && ./install.sh ``` 2. **Add Debug Logging:** ```rust // Add hlog!(debug, ...) in your nodes to trace execution hlog!(debug, "Node state: {:?}", self.state); ``` 3. **Test with Minimal Example:** - Strip down to simplest possible code - Add complexity back one piece at a time - Identify what causes the error 4. **Check System Resources:** ```bash # Check available shared memory (Linux) df -h /dev/shm # Clean stale shared memory if needed horus clean --shm ``` 5. **Report the issue:** - GitHub: https://github.com/softmata/horus/issues - Include: full error message, minimal code example, OS and platform --- ## Next Steps - **[Installation](/getting-started/installation)** - First-time installation guide - **[CLI Reference](/development/cli-reference)** - All horus commands - **[Examples](/rust/examples/basic-examples)** - Working code examples - **[Performance](/performance/performance)** - Optimization tips - **[Testing](/development/testing)** - Test your nodes to prevent runtime errors