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 dashboard or CLI
let max_speed = ctx.params.get_f64("motion.max_speed", 1.5);
let pid_kp = ctx.params.get_f64("control.pid.kp", 1.0);
Benefits:
- No recompilation - Change values without rebuilding
- Live tuning - Adjust while robot is running
- Persistence - Saved to disk automatically
- Sharing - Export/import parameter sets
- Safety - Fallback to defaults if missing
Core Concepts
Parameter Storage
Parameters are stored in a thread-safe map:
Arc<RwLock<BTreeMap<String, Value>>>
Location: ~/.horus/config/params.yaml
Format:
motion:
max_speed: 1.5
acceleration: 0.5
deceleration: 0.8
control:
pid:
kp: 1.0
ki: 0.1
kd: 0.01
safety:
emergency_stop: false
collision_threshold: 0.3
sensors:
lidar_rate_hz: 10
camera_resolution: [1920, 1080]
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)
Hierarchical Keys
Use dot notation for organization:
// Good: Hierarchical structure
ctx.params.get_f64("motion.max_speed", 1.5);
ctx.params.get_f64("control.pid.kp", 1.0);
ctx.params.get_i32("sensors.lidar_rate_hz", 10);
// Avoid: Flat structure
ctx.params.get_f64("max_speed", 1.5);
ctx.params.get_f64("kp", 1.0);
Benefits:
- Clear organization
- Avoid name collisions
- Easy to export/import subsets
- Natural grouping in dashboard
Using Parameters in Nodes
Accessing Parameters
Parameters are available via the NodeInfo context:
node! {
VelocityController {
data {
max_speed: f64 = 1.5,
acceleration: f64 = 0.5
}
init(ctx) {
// Load parameters on initialization
self.max_speed = ctx.params.get_f64("motion.max_speed", 1.5);
self.acceleration = ctx.params.get_f64("motion.acceleration", 0.5);
ctx.log_info(&format!("Max speed: {} m/s", self.max_speed));
ctx.log_info(&format!("Acceleration: {} m/s²", self.acceleration));
}
tick(ctx) {
// Use parameters
let target_velocity = self.max_speed;
// ...
}
}
}
Parameter Methods
Get with default:
// If parameter doesn't exist, returns default value
let speed = ctx.params.get_f64("motion.max_speed", 1.5);
let enabled = ctx.params.get_bool("features.auto_mode", false);
let rate = ctx.params.get_i32("sensors.update_rate_hz", 60);
Get optional:
// Returns Option<T> - None if parameter doesn't exist
if let Some(speed) = ctx.params.get::<f64>("motion.max_speed") {
self.max_speed = speed;
}
Set parameter:
// Update parameter value
ctx.params.set("motion.max_speed", 2.0).ok();
// Set complex types
ctx.params.set("sensors.camera_resolution", vec![1920, 1080]).ok();
Type-specific getters:
// Convenience methods with type inference
let max_speed: f64 = ctx.params.get_f64("motion.max_speed", 1.5);
let node_name: String = ctx.params.get_string("node.name", "default");
let enabled: bool = ctx.params.get_bool("features.enabled", true);
let count: i32 = ctx.params.get_i32("samples.count", 100);
let id: u64 = ctx.params.get_u64("node.id", 0);
Live Reloading
Check for updates every tick:
node! {
AdaptiveController {
data {
max_speed: f64 = 1.5,
last_speed_check: u64 = 0
}
tick(ctx) {
// Check every 60 ticks (~1 second at 60 Hz)
if self.last_speed_check % 60 == 0 {
// Reload parameter
let new_speed = ctx.params.get_f64("motion.max_speed", 1.5);
if new_speed != self.max_speed {
ctx.log_info(&format!("Speed updated: {} {}", self.max_speed, new_speed));
self.max_speed = new_speed;
}
}
self.last_speed_check += 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
ctx.params.set("waypoints", vec![1.0, 2.5, 3.0, 4.5]).ok();
// Get array
let waypoints: Vec<f64> = ctx.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
});
ctx.params.set("network.config", config).ok();
// Get nested values
let ip = ctx.params.get_string("network.config.ip", "localhost");
let port = ctx.params.get_i32("network.config.port", 8080);
Default Parameters
HORUS provides sensible defaults on first run:
# ~/.horus/config/params.yaml (auto-generated)
motion:
max_linear_velocity: 1.0
max_angular_velocity: 1.0
acceleration_limit: 0.5
sensors:
lidar_rate_hz: 10
camera_rate_hz: 30
imu_rate_hz: 100
safety:
emergency_stop_enabled: true
collision_threshold: 0.5
max_tilt_angle_deg: 30.0
control:
pid_linear:
kp: 1.0
ki: 0.1
kd: 0.01
pid_angular:
kp: 1.0
ki: 0.1
kd: 0.01
Customization:
- File is created on first launch
- Edit directly or use dashboard
- Changes persist across restarts
- Delete file to reset to defaults
Managing Parameters
Via Dashboard
Web interface (easiest method):
# Start dashboard
horus dashboard
# 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 Dashboard Guide for API details.
Via Code
Manual save:
// RuntimeParams saves automatically on set()
// But you can force a save
ctx.params.save_to_disk().ok();
Manual load:
// Reload from disk (useful after external edit)
ctx.params.load_from_disk().ok();
Via File Edit
Direct YAML editing:
# Edit parameters file
vim ~/.horus/config/params.yaml
# Changes take effect:
# - Immediately if node reloads
# - On next restart if not
Format:
# Use spaces (2 or 4), not tabs
motion:
max_speed: 2.0 # number
mode: "auto" # string (quotes optional for simple strings)
enabled: true # boolean
sensors:
rates: [10, 30, 100] # array
# Comments are preserved
control:
pid:
kp: 1.0 # Proportional gain
ki: 0.1 # Integral gain
kd: 0.01 # Derivative gain
Common Patterns
PID Controller Tuning
node! {
PIDController {
data {
kp: f64 = 1.0,
ki: f64 = 0.1,
kd: f64 = 0.01,
integral: f64 = 0.0,
last_error: f64 = 0.0
}
init(ctx) {
// Load PID gains
self.kp = ctx.params.get_f64("control.pid.kp", 1.0);
self.ki = ctx.params.get_f64("control.pid.ki", 0.1);
self.kd = ctx.params.get_f64("control.pid.kd", 0.01);
ctx.log_info(&format!("PID: Kp={}, Ki={}, Kd={}", self.kp, self.ki, self.kd));
}
tick(ctx) {
// Compute PID output
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 {
fn compute_error(&self) -> f64 {
// Your error calculation
0.0
}
}
}
}
Tuning workflow:
- Start robot with default gains
- Open dashboard Parameters
- Adjust
control.pid.kp/ki/kdwhile robot runs - Observe behavior in dashboard metrics
- Repeat until satisfactory
- Parameters auto-save - done!
Feature Flags
node! {
AdvancedController {
data {
enable_obstacle_avoidance: bool = false,
enable_path_planning: bool = false,
enable_localization: bool = true
}
init(ctx) {
self.enable_obstacle_avoidance = ctx.params.get_bool("features.obstacle_avoidance", false);
self.enable_path_planning = ctx.params.get_bool("features.path_planning", false);
self.enable_localization = ctx.params.get_bool("features.localization", true);
}
tick(ctx) {
if self.enable_localization {
self.update_localization();
}
if self.enable_obstacle_avoidance {
self.avoid_obstacles();
}
if self.enable_path_planning {
self.plan_path();
}
}
impl {
fn update_localization(&mut self) { /* ... */ }
fn avoid_obstacles(&mut self) { /* ... */ }
fn plan_path(&mut self) { /* ... */ }
}
}
}
Environment-Specific Config
node! {
NetworkNode {
data {
server_url: String = String::from("localhost:8080"),
timeout_ms: u64 = 5000,
retry_count: i32 = 3
}
init(ctx) {
// Load environment-specific parameters
let env = ctx.params.get_string("environment", "development");
match env.as_str() {
"production" => {
self.server_url = ctx.params.get_string("network.prod_url", "prod.example.com:8080");
self.timeout_ms = ctx.params.get_u64("network.prod_timeout_ms", 3000);
},
"staging" => {
self.server_url = ctx.params.get_string("network.staging_url", "staging.example.com:8080");
self.timeout_ms = ctx.params.get_u64("network.staging_timeout_ms", 5000);
},
_ => {
self.server_url = ctx.params.get_string("network.dev_url", "localhost:8080");
self.timeout_ms = ctx.params.get_u64("network.dev_timeout_ms", 10000);
}
}
ctx.log_info(&format!("Connecting to {} (timeout: {}ms)", self.server_url, self.timeout_ms));
}
}
}
Rate Limiting
node! {
SensorPublisher {
data {
publish_rate_hz: u64 = 10,
last_publish: std::time::Instant = std::time::Instant::now()
}
init(ctx) {
self.publish_rate_hz = ctx.params.get_u64("sensors.publish_rate_hz", 10);
ctx.log_info(&format!("Publishing at {} Hz", self.publish_rate_hz));
}
tick(ctx) {
let interval = std::time::Duration::from_millis(1000 / self.publish_rate_hz);
if self.last_publish.elapsed() >= interval {
// Publish sensor data
self.publish_data(ctx);
self.last_publish = std::time::Instant::now();
}
}
impl {
fn publish_data(&mut self, ctx: Option<&mut NodeInfo>) {
// Publishing logic
}
}
}
}
Best Practices
Naming Conventions
Use hierarchical structure:
# Good
motion:
max_linear_velocity: 1.5
max_angular_velocity: 2.0
control:
pid:
kp: 1.0
ki: 0.1
# Bad
max_linear_velocity: 1.5
max_angular_velocity: 2.0
pid_kp: 1.0
pid_ki: 0.1
Use descriptive names:
# Good
sensors:
lidar_scan_rate_hz: 10
camera_resolution_width: 1920
# Bad
sensors:
rate: 10
w: 1920
Use consistent casing:
# Good (snake_case)
motion:
max_speed: 1.5
acceleration_limit: 0.5
# Bad (mixed casing)
motion:
maxSpeed: 1.5
acceleration_limit: 0.5
Always Provide Defaults
Never crash on missing parameters:
// Good - provides fallback
let speed = ctx.params.get_f64("motion.max_speed", 1.5);
// Bad - panics if missing
let speed = ctx.params.get::<f64>("motion.max_speed").unwrap();
Use sensible defaults:
// Good - safe defaults
let emergency_stop = ctx.params.get_bool("safety.emergency_stop", true); // Default to safe state
let max_speed = ctx.params.get_f64("motion.max_speed", 1.0); // Default to slow
// Bad - unsafe defaults
let emergency_stop = ctx.params.get_bool("safety.emergency_stop", false); // Unsafe!
let max_speed = ctx.params.get_f64("motion.max_speed", 100.0); // Too fast!
Document Parameters
Add comments in YAML:
motion:
# Maximum linear velocity in m/s (default: 1.5)
max_linear_velocity: 1.5
# Maximum angular velocity in rad/s (default: 2.0)
max_angular_velocity: 2.0
control:
pid:
# Proportional gain - affects responsiveness (range: 0.1-10.0)
kp: 1.0
# Integral gain - affects steady-state error (range: 0.01-1.0)
ki: 0.1
# Derivative gain - affects damping (range: 0.001-0.1)
kd: 0.01
Add documentation in code:
init(ctx) {
// Load PID gains (tuning range: Kp=0.1-10, Ki=0.01-1, Kd=0.001-0.1)
self.kp = ctx.params.get_f64("control.pid.kp", 1.0);
self.ki = ctx.params.get_f64("control.pid.ki", 0.1);
self.kd = ctx.params.get_f64("control.pid.kd", 0.01);
}
Validate Parameter Values
Add bounds checking:
init(ctx) {
// Load with validation
let speed = ctx.params.get_f64("motion.max_speed", 1.5);
// Clamp to safe range
self.max_speed = speed.max(0.0).min(5.0); // 0-5 m/s
if speed != self.max_speed {
ctx.log_warning(&format!(
"max_speed {} out of range, clamped to {}",
speed, self.max_speed
));
}
}
Export Parameter Sets
Create presets for different scenarios:
# Save current parameters to preset
horus dashboard
# Parameters tab Export Save as "aggressive_tuning.yaml"
# Switch to different preset
cp ~/.horus/config/params.yaml ~/.horus/config/params_backup.yaml
cp aggressive_tuning.yaml ~/.horus/config/params.yaml
# Restart application to load new parameters
Troubleshooting
Parameters Not Loading
Problem: Parameters show default values even though YAML file exists.
Cause: YAML syntax error or file permissions.
Solution:
# Check YAML syntax
cat ~/.horus/config/params.yaml
# Validate YAML
yamllint ~/.horus/config/params.yaml
# Check permissions
ls -la ~/.horus/config/params.yaml
# Should be readable
chmod 644 ~/.horus/config/params.yaml
Parameters Not Saving
Problem: Changes in dashboard don't persist.
Cause: Config directory doesn't exist or lacks write permissions.
Solution:
# Create config directory
mkdir -p ~/.horus/config
# Set permissions
chmod 755 ~/.horus/config
# Manually save from code
ctx.params.save_to_disk().ok();
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 = ctx.params.get_f64("motion.max_speed", 1.5);
Lost Parameters After Update
Problem: Parameters reset to defaults after code update.
Cause: Defaults overwrite file on initialization.
Solution:
Backup parameters before updating:
# Backup
cp ~/.horus/config/params.yaml ~/.horus/config/params_backup.yaml
# After update, restore if needed
cp ~/.horus/config/params_backup.yaml ~/.horus/config/params.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
Hubinstead)
Caching Strategy
Good: Cache and reload periodically
data {
max_speed: f64 = 1.5,
reload_counter: u64 = 0
}
tick(ctx) {
// Reload every 60 ticks (~1 second)
if self.reload_counter % 60 == 0 {
self.max_speed = ctx.params.get_f64("motion.max_speed", 1.5);
}
self.reload_counter += 1;
// Use cached value
let velocity = calculate_velocity(self.max_speed);
}
Bad: Read every tick unnecessarily
tick(ctx) {
// Wasteful - reads same value 60 times per second
let max_speed = ctx.params.get_f64("motion.max_speed", 1.5);
}
Next Steps
- Dashboard Guide - Manage parameters via web interface
- node! Macro Guide - Learn about
init()andtick()hooks - Examples - See real-world parameter usage
- CLI Reference - Command-line parameter tools