Simplifying how to declare parameters in ROS 2

In the Autoware project we make heavy use of parameters to configure our nodes, and we’ve found the API for definiting parameters rather fragile, not easy to have a quick overview of all the parameters defined in a node and some times too verbose.

One approach we’re working on is the use of a YAML schema (see DevOps Dojo: ROS Node Configuration - ROS Node Parameter Coding Guidelines · autowarefoundation · Discussion #3433 · GitHub) that developers can leverage for validating parameters and making sure that they are defined properly.

Another approach is PickNick’s GitHub - PickNikRobotics/generate_parameter_library: Declarative ROS 2 Parameters, which also aims at programmatically declaring parameters.

So, from the Autoware project we’ve realized that this might not be isolated cases, but a common issue in many other ROS projects. Would it make sense for ROS 2 to include some form of mechanism that can make the declaration of parameters easier and more repeatable? We haven’t implemented our approach yet and would appreciate feedback from PickNick (cc: @tylerweaver @davetcoleman ) and from the rest of the community, to see if we could define a way to address this and maybe even have a REP at some point.

cc @mfc @mitsudome-r @kaspermeck-arm

15 Likes

Tagging @Paul.Gesel as he is the author of the Python code in generate_parameter_library that does the code generation. Paul even added support recently for generating for rclpy and markdown docs for parameter structures.

One thing I’ve meant to do for a while is to create a linter for the parameters of a node that can be used in CI to validate configs based on generate_parameter_library that would just generate an empty node that tries to load the parameters. Your json schema approach seems like another useful way to specify if a config is valid for a parameter interface. Is there good tooling that would take the schema and give helpful errors to people trying to fix a config?

My roscon talk about code generation for parameters was accepted. I’d love to help with efforts to make the parameter interface easier to use. Ideally, I’d love something similar to clap-derive for defining the structure and nature of parameters.

3 Likes

this sounds nice and helpful for user application.

probably we could break them down into some features.

  • ROS 2 Parameter support to load and dump with ParameterDescriptor. user can write the yaml file to declare parameters based on ParameterDescriptor attributes, so that is much easier for user to manage parameters with attributes more repeatable.

  • Parameter API wrapper, syntactic sugar for user application and easy access to the parameters. So that user application does not really need to know about Parameter (including node parameters) request/response management.

  • Cached Parameter support. for example, user calls get_parameters(remote_node_name) and all parameters are stored in the local object to access from the user application later on. it depends on the use case if application needs to access up-to-date data for these parameters. if cached flag is enabled, using parameter_event it automatically update the corresponding parameter value in the system.

there could be more features, i am happy to discuss on this enhancement.

1 Like

Interesting.

I think the discussion in this recent post on using ros message in the parameter interface might also be relevant.

From a user-requirements point of view, I second a need:

  1. To load ‘data sets’, i.e. structured data:

    • In one way or another (*)
    • In a variable amount (i.e. a configuration file contains 0 to N data sets of a certain type),
  2. To have this data available:

    • In multiple nodes,
    • Which are distributed across multiple hosts,

    Using multiple configuration files across these hosts is not an option.
    (Mainly since this implies keeping duplicate data across configuration files in sync).

  3. Not all data in a data set is relevant to all nodes.

    E.g. an ‘object_description’ data set could contain:

    • General info, e.g. object_UID and object_name,
    • Perception-related info for the camera_detection node,
    • Picking-related info for the picker_robot node,
    • A destination_identifier for the transportation_node
    • Etc.

    All nodes need the object_UID and object_name, but e.g. the camera_detection node does not need the destination_identifier.

  4. It should be easy to add or edit data.
    E.g. if the data is in a text file, I would prefer:

    • The data-set-related data to be grouped in sections data_set_1, data_set_2, etc.
    • The possible non-data-set-related data to be grouped in sections node_1, node_2, etc.

    As opposed to data-set-related data to be split accross sections common, node_1, node_2, etc


(*) Imo. it does not necessarily _have_ to be through parameters. It might be a misinterpretation, but I sort of conclude from above mentioned thread that parameters are not really intended for structured data?

What I am currently resorting to is:

  • The data sets are loaded by one node,
    • My initial intention was to use parameters,
    • But as this node is inside a gazebo plugin, I resorted to reading from the SDF file,
  • This node decides which data sets are currently active,
  • And it publishes the active data sets using a custom message, which holds a ‘vector of data_set_message
  • All other nodes that need the data subscribe to this topic
    • The publisher QoS is KeepLast(1), reliable and transient_local,
    • So all nodes, including late-joining ones, always get the latest ‘active data sets’,
    • Each time a node receives a new message, it discards the old data sets and loads the new ones.

This works well and suits my current needs, so I am rather pleased with it. But it also feels rather strange as it does not seem to be “the intended way” to do this in ROS 2. It is also not really ‘clean’ (and for some applications probably unacceptable) that the full data sets are sent to all nodes, instead of only the node-specific data.

I think above requirements are pertinent to many applications, so imo. it makes sense to develop a ‘common best practice’ for this, that is part of ROS 2 core.

From our point of view, loading structured data as parameter is a hard requirement.
We tried pushing this, by implementing recursive message support, which would allow us to use tree like parameter messages (see fix: Generate correct code for nested arrays of own type by jmachowinski · Pull Request #748 · ros2/rosidl · GitHub).

At some point there was no progress any more on the merge request and as we needed to move forward, we dumped the whole ros parameter system and implemented our own.

Here is an overview of what we did, as you might wanna pick the good parts of it :

A common package is defined, from which parameters are loaded (config_pkg). This package is shared across machines builds, and may not have local modifications. A parameter file is expected to be found under $(find_shared config_pkg)/namespace/node_name.yaml of the requesting node.

Node parameter files may include other yaml files. This is a common use case for us, as some parameters are shared across multiple nodes, and we did not want to have multiple copies around.
e.g. :

navigation:
    includes:
        - /navigation/navigation.yaml
hal:
    includes:
        - /hal/hardware_config.yaml

pre_spinup_time_info:
  help: time a wheel shall start to spin up before it is touched by a parcel
pre_spinup_time: 0.2

On the node side (cpp only) we use a template system similar to the toMsg/fromMsg one of tf2. This template system allows us to have common headers for common datatypes like Eigen::Vector3d, rclcpp::Duration etc. There are also common headers, to parse containers (vectors, maps) of datatypes.
In the node this looks like this :

minStateStdDev:
  pose:
    x: 0.005
    y: 0.005
    orientation: 1 # orientation in degrees
  velocity:
    x: 0.005
    y: 0.005
    orientation: 1 # orientation in degrees
  size:
    x: 0.005
    y: 0.005
    z: 0.005

struct PoseParameters
{
    Eigen::Vector2d translation;
    cm::Angled rotation;
};

struct StateParameters
{
    PoseParameters pose;
    PoseParameters velocity;
    Eigen::Vector3d size;
};

template<>
struct ParamConversion<passenger_state_estimation::StateParameters>
{
    static passenger_state_estimation::StateParameters from(const YAML::Node& param)
    {
        if (!param.IsDefined())
        {
            throw ConfigException::elementNotDefined();
        }
        checkType(param, YAML::NodeType::Map);

        return passenger_state_estimation::StateParameters{
            getElem<passenger_state_estimation::PoseParameters>(param, "pose"),
            getElem<passenger_state_estimation::PoseParameters>(param, "velocity"),
            getElem<Eigen::Vector3d>(param, "size")};
    }
};

param = nodeBase.declareParameter<StateParameters>("minStateStdDev")
StateParameters p = param->getValue()

Together with this we also developed a web based configuration tool, that allows the colleges that are not from the software department, to change parameter. For this purpose, each yaml parameter may be annotated with an info entry, that is shown in the configuration tool. E.g.:

angular_speed_base_std_dev_info:
  help: base standard deviation for the angular speed
  unit: deg/s
angular_speed_base_std_dev: 10
2 Likes

I’m developing bindings between GStreamer and ROS2, I think GStreamer handles parameters very well.
Gstreamer is intended to be a bit more rigid than ROS, but the way Elements (nodes) self-document with common tools is something to aspire to.
Not everything applies; GStreamer and ROS are nearly opposite ways of making modular pipeline architectures.

GStreamer Elements define Properties statically (by convention, not by necessity) Property declaration happens in a class_init method, so a command line tool called gst-inspect is able to list all of the pads (topic types) and properties(parameters) that are declared by default by partially instantiating the Element.

Gstreamer Properties have descriptions that are easy to forward into a rcl_interfaces::msg::ParameterDescriptor, I do wish rclcpp had message constructors or more helper functions to make parameter declaration less verbose.

Gstreamer Properties have helper methods to reject parameters at runtime.
Rclcpp has totally separate validate and apply callbacks, and these are absolutely critical to reporting that GStreamer rejected a parameter update. I’m not adverse to convenience tools to generate common acceptance patterns, but the API needs a validation callback. We learned that lesson in ROS1 with DynamicReconfigure (I’m glad to see custom validators in the PickNik API)

I’m a bit sick of implementing rclcpp dynamic parameter typing just to permit a user to set a float to “1” instead of “1.0”.
The base class for Gstreamer Properties has a built-in transform() method that allows users to have ultra-specific strongly typed properties, and specify safe conversions to more common types by loading in transformation functions at runtime.

I’d love to see an API for forward declaration of parameters and topics that allows static node inspection.
I have no idea how I’d use that forward declaration API in a dynamic setting like a Node containing a GStreamer Pipeline and exposing properties of elements that come and go.

I think most of these are present in GitHub - PickNikRobotics/generate_parameter_library: Declarative ROS 2 Parameters. It would nice if the community can try the generate_parameter_library solution and see if it satisfies their requirement and/or suggest improvements to it. Rather than generating yet another parameter handling utility.

I just realized a shortcoming: each node’s subscription callback is run independently, so it is not possible to update multiple nodes synchronously. E.g. Node1 could have received and processed a new data set message, while Node2 is still using the old one.

However, I had a look at the regular ROS 2 parameters (if it is possible to synchronize change e.g. through the pre_set / on_set / post_set_parameters_callback or the parameter_listener), but it seems that this also does not provide support for synchronized setting of parameters?

I found this answer suggesting to use lifecycle nodes; imo. that seems a rather cumbersome solution. And you’d still need some ‘lifecycle manager’ that stops and restarts all nodes before/after the parameter change.

The use of lifecycle management is intentional. Because nodes are often running independently, and may even be running on separate computers, the usual problems of distributed systems apply. It is not possible to guarantee that all nodes will update their parameters simultaneously without first moving all nodes to a suitable state to do so. This requires defining “simultaneously” in terms other than wall-clock-style time. So you shift the system to a suitable state, perform the action, then shift the system back to the previous state again.

The alternative is to use something like what distributed databases use, but they don’t attempt to be synchronous, just “eventual”.

1 Like

So how do you keep track of changes to the parameter files accross multiple machines?

I.e. let’s say you make a change to that node_name.yaml file on the primary machine:

  • All packages on the primary machine will read that same parameter file through the $(find_shared config_pkg) variable,
  • But remote machines need an update of their local config folder?

Git. Any parameter change must be committed and reviewed like a code change. The CI pipline rebuilds the software and redeploys it.