Derive ROS parameter description from field doc comments

With this change, adding doc comments to fields of structures used
with `#[derive(RosParams)]` results in those comments being used as
parameter description.

See r2r/examples/parameters_derive.rs for how to use and test this
feature.

*BREAKING CHANGE*

This commit changes r2r public API. Previously Node::params contained
HashMap<String, ParameterValue>, now it contains HashMap<String, Parameter>.

If you previously used the ParameterValue from this HashMap, now you
can get the same by using the .value field of the Parameter structure.
This commit is contained in:
Michal Sojka 2023-09-26 17:36:05 +02:00 committed by Martin Dahl
parent c12a6fdb76
commit 00d7a3db0b
6 changed files with 112 additions and 42 deletions

View File

@ -47,7 +47,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
loop { loop {
println!("node parameters"); println!("node parameters");
params.lock().unwrap().iter().for_each(|(k, v)| { params.lock().unwrap().iter().for_each(|(k, v)| {
println!("{} - {:?}", k, v); println!("{} - {:?}", k, v.value);
}); });
let _elapsed = timer.tick().await.expect("could not tick"); let _elapsed = timer.tick().await.expect("could not tick");
} }

View File

@ -23,6 +23,17 @@ use std::sync::{Arc, Mutex};
// par1: 5.1 // par1: 5.1
// par2: 0 // par2: 0
// //
// ros2 param describe /demo/my_node par1 nested.nested2.par5
// Prints:
// Parameter name: par1
// Type: double
// Description: Parameter description
// Constraints:
// Parameter name: nested.nested2.par5
// Type: integer
// Description: Small parameter
// Constraints:
// Error handling: // Error handling:
// cargo run --example parameters_derive -- --ros-args -p nested.par4:=xxx // cargo run --example parameters_derive -- --ros-args -p nested.par4:=xxx
@ -31,7 +42,9 @@ use std::sync::{Arc, Mutex};
#[derive(RosParams, Default, Debug)] #[derive(RosParams, Default, Debug)]
struct Params { struct Params {
/// Parameter description
par1: f64, par1: f64,
/// Dummy parameter [m/s]
par2: i32, par2: i32,
nested: NestedParams, nested: NestedParams,
} }
@ -45,6 +58,7 @@ struct NestedParams {
#[derive(RosParams, Default, Debug)] #[derive(RosParams, Default, Debug)]
struct NestedParams2 { struct NestedParams2 {
/// Small parameter
par5: i8, par5: i8,
} }

View File

@ -112,7 +112,7 @@ mod context;
pub use context::Context; pub use context::Context;
mod parameters; mod parameters;
pub use parameters::{ParameterValue, RosParams}; pub use parameters::{Parameter, ParameterValue, RosParams};
mod clocks; mod clocks;
pub use clocks::{Clock, ClockType}; pub use clocks::{Clock, ClockType};

View File

@ -35,8 +35,8 @@ use crate::subscribers::*;
/// be called continously. /// be called continously.
pub struct Node { pub struct Node {
context: Context, context: Context,
/// ROS parameter values. /// ROS parameters.
pub params: Arc<Mutex<HashMap<String, ParameterValue>>>, pub params: Arc<Mutex<HashMap<String, Parameter>>>,
node_handle: Box<rcl_node_t>, node_handle: Box<rcl_node_t>,
// the node owns the subscribers // the node owns the subscribers
subscribers: Vec<Box<dyn Subscriber_>>, subscribers: Vec<Box<dyn Subscriber_>>,
@ -146,7 +146,7 @@ impl Node {
let s = unsafe { CStr::from_ptr(*s) }; let s = unsafe { CStr::from_ptr(*s) };
let key = s.to_str().unwrap_or(""); let key = s.to_str().unwrap_or("");
let val = ParameterValue::from_rcl(v); let val = ParameterValue::from_rcl(v);
params.insert(key.to_owned(), val); params.insert(key.to_owned(), Parameter::new(val));
} }
} }
@ -243,7 +243,7 @@ impl Node {
// register all parameters // register all parameters
ps.lock() ps.lock()
.unwrap() .unwrap()
.register_parameters("", &mut self.params.lock().unwrap())?; .register_parameters("", None, &mut self.params.lock().unwrap())?;
} }
let mut handlers: Vec<std::pin::Pin<Box<dyn Future<Output = ()> + Send>>> = Vec::new(); let mut handlers: Vec<std::pin::Pin<Box<dyn Future<Output = ()> + Send>>> = Vec::new();
let (mut event_tx, event_rx) = mpsc::channel::<(String, ParameterValue)>(10); let (mut event_tx, event_rx) = mpsc::channel::<(String, ParameterValue)>(10);
@ -266,19 +266,31 @@ impl Node {
.lock() .lock()
.unwrap() .unwrap()
.get(&p.name) .get(&p.name)
.map(|v| v != &val) .map(|v| v.value != val)
.unwrap_or(true); // changed=true if new .unwrap_or(true); // changed=true if new
let r = if let Some(ps) = &params_struct_clone { let r = if let Some(ps) = &params_struct_clone {
// Update parameter structure
let result = ps.lock().unwrap().set_parameter(&p.name, &val); let result = ps.lock().unwrap().set_parameter(&p.name, &val);
if result.is_ok() { if result.is_ok() {
params.lock().unwrap().insert(p.name.clone(), val.clone()); // Also update Node::params
params
.lock()
.unwrap()
.entry(p.name.clone())
.and_modify(|p| p.value = val.clone());
} }
rcl_interfaces::msg::SetParametersResult { rcl_interfaces::msg::SetParametersResult {
successful: result.is_ok(), successful: result.is_ok(),
reason: result.err().map_or("".into(), |e| e.to_string()), reason: result.err().map_or("".into(), |e| e.to_string()),
} }
} else { } else {
params.lock().unwrap().insert(p.name.clone(), val.clone()); // No parameter structure - update only Node::params
params
.lock()
.unwrap()
.entry(p.name.clone())
.and_modify(|p| p.value = val.clone())
.or_insert(Parameter::new(val.clone()));
rcl_interfaces::msg::SetParametersResult { rcl_interfaces::msg::SetParametersResult {
successful: true, successful: true,
reason: "".into(), reason: "".into(),
@ -316,17 +328,17 @@ impl Node {
.names .names
.iter() .iter()
.map(|n| { .map(|n| {
// First try to get the parameter from the param structure
if let Some(ps) = &params_struct_clone { if let Some(ps) = &params_struct_clone {
ps.lock() if let Ok(value) = ps.lock().unwrap().get_parameter(&n) {
.unwrap() return value;
.get_parameter(&n)
.unwrap_or(ParameterValue::NotSet)
} else {
match params.get(n) {
Some(v) => v.clone(),
None => ParameterValue::NotSet,
} }
} }
// Otherwise get it from node HashMap
match params.get(n) {
Some(v) => v.value.clone(),
None => ParameterValue::NotSet,
}
}) })
.map(|v| v.into_parameter_value_msg()) .map(|v| v.into_parameter_value_msg())
.collect::<Vec<rcl_interfaces::msg::ParameterValue>>(); .collect::<Vec<rcl_interfaces::msg::ParameterValue>>();
@ -384,7 +396,7 @@ impl Node {
.names .names
.iter() .iter()
.map(|name| match params.get(name) { .map(|name| match params.get(name) {
Some(pv) => pv.into_parameter_type(), Some(param) => param.value.into_parameter_type(),
None => rcl_interfaces::msg::ParameterType::PARAMETER_NOT_SET as u8, None => rcl_interfaces::msg::ParameterType::PARAMETER_NOT_SET as u8,
}) })
.collect(); .collect();
@ -402,7 +414,7 @@ impl Node {
fn handle_list_parameters( fn handle_list_parameters(
req: ServiceRequest<rcl_interfaces::srv::ListParameters::Service>, req: ServiceRequest<rcl_interfaces::srv::ListParameters::Service>,
params: &Arc<Mutex<HashMap<String, ParameterValue>>>, params: &Arc<Mutex<HashMap<String, Parameter>>>,
) -> future::Ready<()> { ) -> future::Ready<()> {
use rcl_interfaces::srv::ListParameters; use rcl_interfaces::srv::ListParameters;
@ -445,26 +457,21 @@ impl Node {
fn handle_desc_parameters( fn handle_desc_parameters(
req: ServiceRequest<rcl_interfaces::srv::DescribeParameters::Service>, req: ServiceRequest<rcl_interfaces::srv::DescribeParameters::Service>,
params: &Arc<Mutex<HashMap<String, ParameterValue>>>, params: &Arc<Mutex<HashMap<String, Parameter>>>,
) -> future::Ready<()> { ) -> future::Ready<()> {
use rcl_interfaces::msg::ParameterDescriptor; use rcl_interfaces::msg::ParameterDescriptor;
use rcl_interfaces::srv::DescribeParameters; use rcl_interfaces::srv::DescribeParameters;
let mut descriptors = Vec::<ParameterDescriptor>::new(); let mut descriptors = Vec::<ParameterDescriptor>::new();
let params = params.lock().unwrap(); let params = params.lock().unwrap();
for name in &req.message.names { for name in &req.message.names {
if let Some(pv) = params.get(name) { let default = Parameter::empty();
let param = params.get(name).unwrap_or(&default);
descriptors.push(ParameterDescriptor { descriptors.push(ParameterDescriptor {
name: name.clone(), name: name.clone(),
type_: pv.into_parameter_type(), type_: param.value.into_parameter_type(),
description: param.description.to_string(),
..Default::default() ..Default::default()
}); });
} else {
// parameter not found, but undeclared allowed, so return empty
descriptors.push(ParameterDescriptor {
name: name.clone(),
..Default::default()
});
}
} }
req.respond(DescribeParameters::Response { descriptors }) req.respond(DescribeParameters::Response { descriptors })
.expect("could not send reply to describe parameters request"); .expect("could not send reply to describe parameters request");

View File

@ -160,6 +160,29 @@ impl ParameterValue {
} }
} }
/// ROS parameter.
pub struct Parameter {
pub value: ParameterValue,
pub description: &'static str,
// TODO: Add other fields like min, max, step. Use field
// attributes for defining them.
}
impl Parameter {
pub fn new(value: ParameterValue) -> Self {
Self {
value,
description: "",
}
}
pub fn empty() -> Self {
Self {
value: ParameterValue::NotSet,
description: "",
}
}
}
/// Trait for use it with /// Trait for use it with
/// [`Node::make_derived_parameter_handler()`](crate::Node::make_derived_parameter_handler()). /// [`Node::make_derived_parameter_handler()`](crate::Node::make_derived_parameter_handler()).
/// ///
@ -167,7 +190,7 @@ impl ParameterValue {
/// `parameters_derive.rs` example. /// `parameters_derive.rs` example.
pub trait RosParams { pub trait RosParams {
fn register_parameters( fn register_parameters(
&mut self, prefix: &str, params: &mut HashMap<String, ParameterValue>, &mut self, prefix: &str, param: Option<Parameter>, params: &mut HashMap<String, Parameter>,
) -> Result<()>; ) -> Result<()>;
fn get_parameter(&mut self, param_name: &str) -> Result<ParameterValue>; fn get_parameter(&mut self, param_name: &str) -> Result<ParameterValue>;
fn set_parameter(&mut self, param_name: &str, param_val: &ParameterValue) -> Result<()>; fn set_parameter(&mut self, param_name: &str, param_val: &ParameterValue) -> Result<()>;
@ -178,16 +201,18 @@ macro_rules! impl_ros_params {
($type:path, $param_value_type:path, $to_param_conv:path, $from_param_conv:path) => { ($type:path, $param_value_type:path, $to_param_conv:path, $from_param_conv:path) => {
impl RosParams for $type { impl RosParams for $type {
fn register_parameters( fn register_parameters(
&mut self, prefix: &str, params: &mut HashMap<String, ParameterValue>, &mut self, prefix: &str, param: Option<Parameter>,
params: &mut HashMap<String, Parameter>,
) -> Result<()> { ) -> Result<()> {
if let Some(param_val) = params.get(prefix) { if let Some(cli_param) = params.get(prefix) {
// Apply parameter value if set from command line or launch file // Apply parameter value if set from command line or launch file
self.set_parameter("", param_val) self.set_parameter("", &cli_param.value)
.map_err(|e| e.update_param_name(prefix))?; .map_err(|e| e.update_param_name(prefix))?;
} else {
// Insert missing parameter with its default value
params.insert(prefix.to_owned(), $param_value_type($to_param_conv(self)?));
} }
// Insert (or replace) the parameter with filled-in description etc.
let mut param = param.unwrap();
param.value = $param_value_type($to_param_conv(self)?);
params.insert(prefix.to_owned(), param);
Ok(()) Ok(())
} }

View File

@ -26,7 +26,8 @@ pub fn derive_r2r_params(input: proc_macro::TokenStream) -> proc_macro::TokenStr
fn register_parameters( fn register_parameters(
&mut self, &mut self,
prefix: &str, prefix: &str,
params: &mut ::std::collections::hash_map::HashMap<String, ::r2r::ParameterValue>, desc: ::std::option::Option<::r2r::Parameter>,
params: &mut ::std::collections::hash_map::HashMap<String, ::r2r::Parameter>,
) -> ::r2r::Result<()> { ) -> ::r2r::Result<()> {
let prefix = if prefix.is_empty() { let prefix = if prefix.is_empty() {
String::from("") String::from("")
@ -79,9 +80,14 @@ fn get_register_calls(data: &Data) -> TokenStream {
let field_matches = fields.named.iter().map(|f| { let field_matches = fields.named.iter().map(|f| {
let name = &f.ident; let name = &f.ident;
let format_str = format!("{{prefix}}{}", name.as_ref().unwrap()); let format_str = format!("{{prefix}}{}", name.as_ref().unwrap());
let desc = get_field_doc(f);
quote_spanned! { quote_spanned! {
f.span() => f.span() =>
self.#name.register_parameters(&format!(#format_str), params)?; let param = ::r2r::Parameter {
value: ::r2r::ParameterValue::NotSet, // will be set for leaf params by register_parameters() below
description: #desc,
};
self.#name.register_parameters(&format!(#format_str), Some(param), params)?;
} }
}); });
quote! { quote! {
@ -94,6 +100,24 @@ fn get_register_calls(data: &Data) -> TokenStream {
} }
} }
fn get_field_doc(f: &syn::Field) -> String {
if let Some(doc) = f
.attrs
.iter()
.find(|&attr| attr.path().get_ident().is_some_and(|id| id == "doc"))
{
match &doc.meta.require_name_value().unwrap().value {
::syn::Expr::Lit(exprlit) => match &exprlit.lit {
::syn::Lit::Str(s) => s.value().trim().to_owned(),
_ => unimplemented!(),
},
_ => unimplemented!(),
}
} else {
"".to_string()
}
}
// Generate match arms for RosParams::update_parameters() // Generate match arms for RosParams::update_parameters()
fn param_matches_for(call: TokenStream, data: &Data) -> TokenStream { fn param_matches_for(call: TokenStream, data: &Data) -> TokenStream {
match *data { match *data {