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:
// Hardcoded - requires recompile to change
let max_speed = 1.5;
let pid_kp = 1.0;
With parameters:
// 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:
Arc<RwLock<BTreeMap<String, Value>>>
Location: .horus/config/params.yaml (relative to your project directory)
Format:
# 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 asValue::Number) - Strings -
String(stored asValue::String) - Booleans -
bool(stored asValue::Bool) - Arrays -
Vec<T>(stored asValue::Array) - Objects -
HashMap<String, T>(stored asValue::Object)
Key Organization
Keys are stored as flat strings in a BTreeMap (sorted alphabetically). Use descriptive names with underscores:
// 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:
// 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:
use horus::prelude::*;
pub struct VelocityController {
params: RuntimeParams,
max_speed: f64,
acceleration: f64,
}
impl VelocityController {
fn new() -> Result<Self> {
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):
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<T>):
// Returns None if parameter doesn't exist or type doesn't match
if let Some(speed) = self.params.get::<f64>("max_speed") {
self.max_speed = speed;
}
Generic get with default:
// Returns default if parameter doesn't exist
let rate: i32 = self.params.get_or("update_rate", 60);
Set parameter:
// 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:
self.params.has("max_speed"); // bool — check if key exists
self.params.list_keys(); // Vec<String> — all parameter keys
self.params.get_all(); // BTreeMap<String, Value> — all params
self.params.remove("old_key"); // Option<Value> — remove and return
self.params.reset()?; // Reset all params to defaults
Persistence:
// 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:
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<RwLock>), but avoid reading hundreds of parameters every tick. Cache values and reload periodically.
Complex Parameter Types
Arrays:
// Set array
self.params.set("waypoints", vec![1.0, 2.5, 3.0, 4.5])?;
// Get array
let waypoints: Vec<f64> = self.params
.get::<Vec<serde_json::Value>>("waypoints")
.map(|v| v.iter().filter_map(|x| x.as_f64()).collect())
.unwrap_or_default();
Objects:
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::<serde_json::Map<String, serde_json::Value>>("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:
# .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:
- Defaults are loaded if no params file exists in the project
- Edit the YAML file directly, use the monitor, or use
horus param set - Call
params.reset()to restore defaults - Call
params.save_to_disk()to persist changes
Managing Parameters
Via Monitor
Web interface (easiest method):
# 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 for API details.
Via Code
Save to disk:
// Parameters are NOT auto-saved on set() — you must save explicitly
self.params.save_to_disk()?;
Load from disk:
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:
# 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:
# Edit parameters file (project-relative)
vim .horus/config/params.yaml
# Changes take effect on next RuntimeParams::init() or load_from_disk()
Format:
# 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
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:
- Start robot with default gains
- Open monitor → Parameters
- Adjust
pid_kp/pid_ki/pid_kdwhile robot runs - Observe behavior in monitor metrics
- Repeat until satisfactory
- Save with
horus param saveorparams.save_to_disk()
Feature Flags
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
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
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:
# Good
lidar_scan_rate: 10
camera_resolution_width: 1920
# Bad
rate: 10
w: 1920
Use consistent snake_case:
# 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:
// Good - provides fallback
let speed = self.params.get_or("max_speed", 1.5);
// Bad - panics if missing
let speed = self.params.get::<f64>("max_speed").unwrap();
Use sensible defaults:
// 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:
# 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:
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():
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<String>) | Value must be one of allowed strings |
MinLength(usize) | Minimum string/array length |
MaxLength(usize) | Maximum string/array length |
RequiredKeys(Vec<String>) | Object must contain these keys |
Read-only parameters:
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:
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:
# 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:
# 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:
# 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:
# Wrong - string
max_speed: "1.5"
# Correct - number
max_speed: 1.5
Or use type conversion in code:
// 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:
# 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<RwLock<BTreeMap>>:
- 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
Topicinstead)
Caching Strategy
Good: Cache and reload periodically
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
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:
// 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 - Manage parameters via web interface
- Core Concepts - Learn about nodes, topics, and scheduling
- Examples - See real-world parameter usage
- CLI Reference -
horus paramcommand reference