Parameter

Source code.

This chapter introduces how to use parameters. A node can have parameters as follows.

graph TD;
    NodeA --> ParamA1:i64;
    NodeA --> ParamA2:bool;
    NodeA --> ParamA3:String;
    NodeB --> ParamB1:f64;
    NodeB --> ParamB2:f64;

In this figure, NodeA has 3 parameters whose types are i64, bool, and String, respectively, and NodeB has 2 parameters whose types are f64. Parameters can be read/write from external nodes.

To receive a notification of updating a parameter, we can use a callback function or async/await. In this chapter, we will explain how to handle it.

Packages

We will prepare 2 packages as follows.

$ mkdir params
$ cd params
$ cargo new param_server
$ cargo new async_param_server

param_server explains a callback based parameter handling, and async_param_server explains an async/await based.

To manage 2 packages, let's prepare Cargo.toml file for a workspace as follows.

params/Cargo.toml

[workspace]
members = ["param_server", "async_param_server"]

The following table shows files we use in this chapter.

FileWhat?
params/param_server/callback based parameter server
params/async_param_server/async/await based parameter server
params/Cargo.tomlworkspace configuration

Type of Parameter

Before explaining handlers, let's see types of parameters.

The type of parameter value is defined by safe_drive::parameter::Value as follows.

#![allow(unused)]
fn main() {
pub enum Value {
    NotSet,
    Bool(bool),
    I64(i64),
    F64(f64),
    String(String),
    VecBool(Vec<bool>),
    VecI64(Vec<i64>),
    VecU8(Vec<u8>),
    VecF64(Vec<f64>),
    VecString(Vec<String>),
}
}

This means that i64 is valid, but i8, i32, and other user defined types are invalid.

A parameter is associated with a descriptor of safe_drive::parameter::Descriptor as follows.

#![allow(unused)]
fn main() {
pub struct Descriptor {
    pub description: String,
    pub additional_constraints: String,
    pub read_only: bool,
    pub dynamic_typing: bool,
    pub floating_point_range: Option<FloatingPointRange>,
    pub integer_range: Option<IntegerRange>,
}
}

So, a parameter and parameters can be represented by safe_drive::parameter::{Parameter, Parameters} as follows.

#![allow(unused)]
fn main() {
pub struct Parameter {
    pub descriptor: Descriptor,
    pub value: Value,
}

pub struct Parameters { /* omitted private fields */ }
}

and a parameter server can be represented by safe_drive::parameter::ParameterServer as follows.

#![allow(unused)]
fn main() {
pub struct ParameterServer {
    pub params: Arc<RwLock<Parameters>>
    // omitted private fields
}
}

In summary, a parameter server can be represented as follows.

graph TD;
    ParameterServer --> Parameters;
    Parameters --> Parameter;
    Parameter --> Descriptor;
    Parameter --> Value;

Parameter Setting and Waiting Update by Callback

We will explain how to set parameters, and how to wait updating by using a callback function.

Edit param_server/Cargo.toml

Add safe_drive to the dependencies section of Cargo.toml as follows.

[dependencies]
safe_drive = "0.4"

Edit param_server/src/main.rs

param_server can be implemented as follows. This sets 2 parameters up and waits updating.

use safe_drive::{
    context::Context,
    error::DynError,
    logger::Logger,
    parameter::{Descriptor, Parameter, Value},
    pr_info,
};

fn main() -> Result<(), DynError> {
    // Create a context and a node.
    let ctx = Context::new()?;
    let node = ctx.create_node("param_server", None, Default::default())?;

    // Create a parameter server.
    let param_server = node.create_parameter_server()?;
    {
        // Set parameters.
        let mut params = param_server.params.write(); // Write lock

        // Statically typed parameter.
        params.set_parameter(
            "my_flag".to_string(),                     // parameter name
            Value::Bool(false),                        // value
            false,                                     // read only?
            Some("my flag's description".to_string()), // description
        )?;

        // Dynamically typed parameter.
        params.set_dynamically_typed_parameter(
            "my_dynamic_type_flag".to_string(), // parameter name
            Value::Bool(false),                 // value
            false,                              // read only?
            Some("my dynamic type flag's description".to_string()), // description
        )?;

        // Add Directly from Parameter struct
        let parameter_to_set = Parameter {
            descriptor: Descriptor {
                description: "my parameter description".to_string(), // parameter description
                additional_constraints: "my parameter addutional_constraints".to_string(), // parameter additional constraints
                read_only: false,           // read only ?
                dynamic_typing: false,      // static or Dynamic
                floating_point_range: None, // floating point range
                integer_range: None,        // integer point range
            },
            value: Value::Bool(false), // value
        };
        params.add_parameter(
            ("my parameter").to_string(), // name
            parameter_to_set,             // parameter
        )?;
    }

    // Create a logger and a selector.
    let logger = Logger::new("param_server");
    let mut selector = ctx.create_selector()?;

    // Add a callback function to the parameter server.
    selector.add_parameter_server(
        param_server,
        Box::new(move |params, updated| {
            // Print updated parameters.
            let mut keys = String::new();
            for key in updated.iter() {
                let value = &params.get_parameter(key).unwrap().value;
                keys = format!("{keys}{key} = {}, ", value);
            }
            pr_info!(logger, "updated parameters: {keys}");
        }),
    );

    // Spin.
    loop {
        selector.wait()?;
    }
}

param_server in Detail

First of all, we have to create a parameter server by create_parameter_server() method as follows.

#![allow(unused)]
fn main() {
// Create a parameter server.
let param_server = node.create_parameter_server()?;
}

Then set parameters as follows. To update parameters, we have to acquire a write lock for mutual exclusion by write() method.

#![allow(unused)]
fn main() {
{
    // Set parameters.
    let mut params = param_server.params.write(); // Write lock

    // Statically typed parameter.
    params.set_parameter(
        "my_flag".to_string(),                     // parameter name
        Value::Bool(false),                        // value
        false,                                     // read only?
        Some("my flag's description".to_string()), // description
    )?;

    // Dynamically typed parameter.
    params.set_dynamically_typed_parameter(
        "my_dynamic_type_flag".to_string(), // parameter name
        Value::Bool(false),                 // value
        false,                              // read only?
        Some("my dynamic type flag's description".to_string()), // description
    )?;

    // Add Directly from Parameter struct
    let parameter_to_set = Parameter {
        descriptor: Descriptor {
            description: "my parameter description".to_string(), // parameter description
            additional_constraints: "my parameter addutional_constraints".to_string(), // parameter additional constraints
            read_only: false,           // read only ?
            dynamic_typing: false,      // static or Dynamic
            floating_point_range: None, // floating point range
            integer_range: None,        // integer point range
        },
        value: Value::Bool(false), // value
    };
    params.add_parameter(
        ("my parameter").to_string(), // name
        parameter_to_set,             // parameter
    )?;
}
}

To set a statically typed parameters, use set_parameter(). A statically typed parameter cannot be updated by a value whose type is different from original type.

To set a dynamically typed parameters, use set_dynamically_typed_parameter(). A dynamically typed parameter can be updated by an arbitrary type.

To set directly from the Parameter struct, use add_parameter(). A Parameter struct can contain additional_constraints.

Finally, register a callback function to wait updating parameters as follows.

#![allow(unused)]
fn main() {
// Add a callback function to the parameter server.
selector.add_parameter_server(
    param_server,
    Box::new(move |params, updated| {
        // Print updated parameters.
        let mut keys = String::new();
        for key in updated.iter() {
            let value = &params.get_parameter(key).unwrap().value;
            keys = format!("{keys}{key} = {}, ", value);
        }
        pr_info!(logger, "updated parameters: {keys}");
    }),
);

// Spin.
loop {
    selector.wait()?;
}
}

1st argument of the closure, params, is a value of parameters, and 2nd argument, updated is a value containing updated parameters.

Execute param_server

Then, let's execute param_server and get/set a parameter. First, execute param_server in a terminal application window as follows.

$ cargo run --bin param_server
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/param_server`
[INFO] [1669873997.622330908] [param_server]: updated parameters: my_flag = true,

Then, get and set the parameter by using ros2 command in another terminal application window as follows.

$ ros2 param get param_server my_flag
Boolean value is: False
$ ros2 param set param_server my_flag True
Set parameter successful
$ ros2 param get param_server my_flag
Boolean value is: True
$ ros2 param set param_server my_flag 10
Setting parameter failed: failed type checking: dst = Bool, src = I64

We can get and set boolean values, but integer value cannot be set because my_flag is boolean type and statically typed.


Asynchronous Wait

Then, we explain async/await based parameter server.

Edit async_param_server/Cargo.toml

Add safe_drive and tokio to the dependencies section of Cargo.toml as follows.

[dependencies]
safe_drive = "0.4"
tokio = { version = "1", features = ["full"] }

Edit async_param_server/src/main.rs

async_param_server can be implemented as follows. This sets 2 parameters up and waits updating.

use safe_drive::{
    context::Context,
    error::DynError,
    logger::Logger,
    parameter::{Descriptor, Parameter, Value},
    pr_info,
};

#[tokio::main]
async fn main() -> Result<(), DynError> {
    // Create a context and a node.
    let ctx = Context::new()?;
    let node = ctx.create_node("async_param_server", None, Default::default())?;

    // Create a parameter server.
    let mut param_server = node.create_parameter_server()?;
    {
        // Set parameters.
        let mut params = param_server.params.write(); // Write lock

        // Statically typed parameter.
        params.set_parameter(
            "my_flag".to_string(),                     // parameter name
            Value::Bool(false),                        // value
            false,                                     // read only?
            Some("my flag's description".to_string()), // description
        )?;

        // Dynamically typed parameter.
        params.set_dynamically_typed_parameter(
            "my_dynamic_type_flag".to_string(), // parameter name
            Value::Bool(false),                 // value
            false,                              // read only?
            Some("my dynamic type flag's description".to_string()), // description
        )?;

        // Add Directly from Parameter struct
        let parameter_to_set = Parameter {
            descriptor: Descriptor {
                description: "my parameter description".to_string(), // parameter description
                additional_constraints: "my parameter addutional_constraints".to_string(), // parameter additional constraints
                read_only: false,           // read only ?
                dynamic_typing: false,      // static or Dynamic
                floating_point_range: None, // floating point range
                integer_range: None,        // integer point range
            },
            value: Value::Bool(false), // value
        };
        params.add_parameter(
            ("my parameter").to_string(), // name
            parameter_to_set,             // parameter
        )?;
    }

    // Create a logger.
    let logger = Logger::new("async_param_server");

    loop {
        // Wait update asynchronously.
        let updated = param_server.wait().await?;

        let params = param_server.params.read(); // Read lock

        // Print updated parameters.
        let mut keys = String::new();
        for key in updated.iter() {
            let value = &params.get_parameter(key).unwrap().value;
            keys = format!("{keys}{key} = {}, ", value);
        }
        pr_info!(logger, "updated parameters: {keys}");
    }
}

Important things are only following 2 lines.

#![allow(unused)]
fn main() {
// Wait update asynchronously.
let updated = param_server.wait().await?;

let params = param_server.params.read(); // Read lock
}

To wait updating, use wait().await and acquire a read lock by read() method to read parameters. wait().await returns updated parameters.