Tutorial 7: Parameters Deep Dive (C++)

Parameters let you change robot behavior without recompiling. Tune PID gains, adjust speed limits, enable/disable features — from a YAML file read at startup, or from your own code while the robot is running.

What You'll Learn

  • horus::Params for typed key-value storage
  • get<T>() with defaults for safe access
  • Loading startup values from .horus/config/params.yaml (written by horus param set)
  • Organizing parameters by subsystem

Basic Usage

#include <horus/horus.hpp>
using namespace horus::literals;

class TunableController : public horus::Node {
public:
    TunableController(horus::Params& params)
        : Node("tunable_ctrl"), params_(params)
    {
        cmd_sub_ = subscribe<horus::msg::CmdVel>("cmd_vel");
        out_pub_ = advertise<horus::msg::CmdVel>("motor.cmd");
    }

    void init() override {
        // Read a startup value back — get<std::string>() falls back to "unnamed"
        horus::log::info(name(), "robot: " + params_.get<std::string>("robot_name", "unnamed"));
    }

    void tick() override {
        // Read parameters every tick — picks up any set() done elsewhere
        // in this process (there is no live channel from the CLI)
        double max_speed = params_.get<double>("max_speed", 0.5);
        double gain      = params_.get<double>("controller_gain", 0.8);
        bool   enabled   = params_.get<bool>("motor_enabled", true);

        if (!enabled) {
            horus::msg::CmdVel stop{};
            out_pub_->send(stop);
            return;
        }

        auto cmd = cmd_sub_->recv();
        if (!cmd) return;

        double scaled = cmd->get()->linear * gain;
        scaled = std::min(scaled, max_speed);

        horus::msg::CmdVel out{};
        out.linear = static_cast<float>(scaled);
        out_pub_->send(out);
    }

private:
    horus::Params& params_;
    horus::Subscriber<horus::msg::CmdVel>* cmd_sub_;
    horus::Publisher<horus::msg::CmdVel>*  out_pub_;
};

int main() {
    horus::Params params;

    // Override whatever params.yaml supplied for these keys
    params.set("max_speed", 0.5);
    params.set("controller_gain", 0.8);
    params.set("motor_enabled", true);
    params.set("robot_name", "atlas");

    horus::Scheduler sched;
    sched.tick_rate(100_hz);

    TunableController ctrl(params);
    sched.add(ctrl).order(10).build();

    sched.spin();
}

Where startup values come from

Constructing horus::Params layers three sources, lowest precedence first: HORUS's built-in defaults, then .horus/config/params.yaml (the file horus param set writes), then any HORUS_PARAM_* environment variables. That happens once, in the constructor — there is no file watcher and no live channel from the horus param CLI, so a horus param set issued while the node is running changes nothing until you restart the process.

Note the ordering in main() above: the params.set(...) calls run after the file has been loaded, so they overwrite whatever params.yaml held for those keys. Drop them and rely on the fallbacks in get<T>("key", default) if you want the YAML file to win.

Supported Types

Typeset()get<T>()get_*()
doubleset("key", 1.5)get<double>("key", 0.0)get_f64("key")optional<double>
int64_tset("key", int64_t(42))get<int64_t>("key", 0)get_i64("key")optional<int64_t>
boolset("key", true)get<bool>("key", false)get_bool("key")optional<bool>
stringset("key", "value")get<std::string>("key", "")get_string("key")optional<string>

Organizing Parameters

Group by subsystem:

// Locomotion
params.set("loco.max_speed", 0.5);
params.set("loco.max_angular", 1.0);
params.set("loco.wheel_base", 0.3);

// PID
params.set("pid.kp", 2.0);
params.set("pid.ki", 0.1);
params.set("pid.kd", 0.05);

// Safety
params.set("safety.min_distance", 0.3);
params.set("safety.estop_enabled", true);

Pattern: Parameter Validation

void init() override {
    double kp = params_.get<double>("pid.kp", -1.0);
    if (kp < 0) {
        horus::log::error(name(), "pid.kp must be >= 0");
        return;
    }
    double max_speed = params_.get<double>("loco.max_speed", 0.5);
    if (max_speed > 2.0) {
        horus::log::warn(name(), "max_speed > 2.0 m/s — are you sure?");
    }
}

The first argument to horus::log::info / warn / error is the node name, not a free-form category — it is what horus log <node> filters on. Inside a node, pass name() so the message lands under the node that emitted it (tunable_ctrl here), even though the key being checked lives under a different prefix.

Key Takeaways

  • Read params every tick — captures set() calls made elsewhere in the process
  • Startup values load once, in the Params constructor — restart to pick up a horus param set
  • Always provide defaults: get<double>("key", 0.0) never fails
  • Use dotted naming (pid.kp, loco.max_speed) to organize
  • Validate in init() — log warnings for dangerous values
  • horus::Params is thread-safe for concurrent read/write