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

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

// 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

// Option-based (returns None if missing or type mismatch)
let speed: Option<f64> = 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

// 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

// 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:

KeyDefaultDescription
tick_rate30Default scheduler tick rate (Hz)
max_memory_mb512Memory limit
max_speed1.0Maximum linear speed (m/s)
max_angular_speed1.0Maximum angular speed (rad/s)
acceleration_limit0.5Acceleration limit (m/s²)
lidar_rate10LiDAR scan rate (Hz)
camera_fps30Camera frame rate
sensor_timeout_ms1000Sensor timeout (ms)
emergency_stop_distance0.3E-stop trigger distance (m)
collision_threshold0.5Collision detection threshold (m)
pid_kp1.0PID proportional gain
pid_ki0.1PID integral gain
pid_kd0.05PID derivative gain

Usage in Nodes

use horus::prelude::*;

struct MotorController {
    params: RuntimeParams,
}

impl MotorController {
    fn new() -> Result<Self> {
        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

import horus

p = horus.Params()
kp = p["pid_kp"]        # 1.0
p["pid_kp"] = 2.5
p.save()

See Python Bindings — Runtime Parameters for full Python API.

API Reference

MethodReturnsDescription
new()Result<Self>Load from .horus/config/params.yaml or defaults
get::<T>(key)Option<T>Typed get, None if missing
get_or::<T>(key, default)TGet with default
get_typed::<T>(key)Result<T>Get with error reporting
set(key, value)Result<()>Set (validates if metadata exists)
has(key)boolCheck existence
list_keys()Vec<String>All parameter names
remove(key)Option<Value>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)u64Version 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.

RuleApplies ToDescription
MinValue(f64)NumbersValue must be >= minimum
MaxValue(f64)NumbersValue must be <= maximum
Range(f64, f64)NumbersValue must be within [min, max]
RegexPattern(String)StringsValue must match regex pattern
Enum(Vec<String>)StringsValue must be one of the allowed values
MinLength(usize)Strings/ArraysMinimum length/element count
MaxLength(usize)Strings/ArraysMaximum length/element count
RequiredKeys(Vec<String>)ObjectsJSON object must contain all listed keys

When a set() violates a rule, it returns Err(HorusError::Validation(...)) with a descriptive message.

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.

FieldTypeDescription
descriptionOption<String>Human-readable description
unitOption<String>Unit of measurement (e.g., "m/s", "Hz", "rad")
validationVec<ValidationRule>Validation rules (checked on set)
read_onlyboolIf true, set() is rejected
// 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:

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:

// 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:

pid.kp: 2.5
pid.ki: 0.1
pid.kd: 0.05
max_speed: 1.0
mode: "auto"

Method Reference

new

pub fn new() -> HorusResult<Self>

Create a parameter store. Loads defaults and any existing values from .horus/config/params.yaml.

Example:

let params = RuntimeParams::new()?;

get

pub fn get<T: Deserialize>(&self, key: &str) -> Option<T>

Get a parameter value, deserialized to type T. Returns None if the key doesn't exist.

Example:

let speed: Option<f64> = params.get("max_speed");

get_typed

pub fn get_typed<T: Deserialize>(&self, key: &str) -> HorusResult<T>

Get a parameter, returning an error if missing or wrong type. Use when the parameter is required.

Example:

let kp: f64 = params.get_typed("pid_kp")?; // error if missing

get_or

pub fn get_or<T: Deserialize>(&self, key: &str, default: T) -> T

Get a parameter with a fallback default.

Example:

let speed = params.get_or("max_speed", 1.5); // 1.5 if not set

set

pub fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), HorusError>

Set a parameter. Validates against metadata constraints (min/max, regex, read-only) if configured. Logs the change to the audit log.

Errors: ValidationError::OutOfRange if value violates constraints, or if the key is read-only.

Example:

params.set("pid_kp", 2.5)?;
params.set("max_speed", 0.8)?;

set_with_version

pub fn set_with_version<T: Serialize>(&self, key: &str, value: T, expected_version: u64) -> Result<(), HorusError>

Optimistic locking — set only if the parameter hasn't been modified since expected_version. Prevents concurrent edit conflicts.

Example:

let version = params.get_version("pid_kp");
// ... compute new value ...
params.set_with_version("pid_kp", new_kp, version)?; // fails if someone else changed it

list_keys

pub fn list_keys(&self) -> Vec<String>

Returns all parameter names.

Example:

for key in params.list_keys() {
    println!("{} = {:?}", key, params.get::<serde_json::Value>(&key));
}

has

pub fn has(&self, key: &str) -> bool

Check if a parameter exists.


remove

pub fn remove(&self, key: &str) -> Option<Value>

Remove a parameter. Returns the old value if it existed.


reset

pub fn reset(&self) -> Result<(), HorusError>

Reset all parameters to defaults. Clears user-set values.


get_metadata

pub fn get_metadata(&self, key: &str) -> Option<ParamMetadata>

Get metadata for a parameter: description, unit, read-only flag, validation rules.

Example:

if let Some(meta) = params.get_metadata("max_speed") {
    println!("Unit: {:?}, Read-only: {}", meta.unit, meta.read_only);
}

get_version

pub fn get_version(&self, key: &str) -> u64

Get the current version counter for a parameter. Used with set_with_version() for optimistic locking.


See Also