From 0e69c26296be7dbe1308995e05e63e93b0ea568e Mon Sep 17 00:00:00 2001 From: "Rahul K.A" Date: Mon, 18 Aug 2025 23:17:17 -0700 Subject: [PATCH 1/5] chore: Begin adding parameter support for rclcpp Signed-off-by: Rahul K.A --- rclpy/rclpy/node.py | 1 + rclpy/rclpy/parameter.py | 41 +++++++++++++++++++++++++++++++++++++--- rclpy/src/rclpy/node.cpp | 5 ++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/rclpy/rclpy/node.py b/rclpy/rclpy/node.py index 1c2d8f588..4e7c05b06 100644 --- a/rclpy/rclpy/node.py +++ b/rclpy/rclpy/node.py @@ -14,6 +14,7 @@ import math import time +import yaml from types import TracebackType from typing import Any diff --git a/rclpy/rclpy/parameter.py b/rclpy/rclpy/parameter.py index 1fd7dab76..c35768d60 100644 --- a/rclpy/rclpy/parameter.py +++ b/rclpy/rclpy/parameter.py @@ -40,14 +40,14 @@ # Mypy does not handle string literals of array.array[int/str/float] very well # So if user has newer version of python can use proper array types. if sys.version_info > (3, 9): - AllowableParameterValue = Union[None, bool, int, float, str, + AllowableParameterValue = Union[None, bool, int, float, str, dict, list[bytes], Tuple[bytes, ...], list[bool], Tuple[bool, ...], list[int], Tuple[int, ...], array.array[int], list[float], Tuple[float, ...], array.array[float], list[str], Tuple[str, ...], array.array[str]] else: - AllowableParameterValue = Union[None, bool, int, float, str, + AllowableParameterValue = Union[None, bool, int, float, str, dict, List[bytes], Tuple[bytes, ...], List[bool], Tuple[bool, ...], List[int], Tuple[int, ...], 'array.array[int]', @@ -71,6 +71,7 @@ class Type(IntEnum): INTEGER = ParameterType.PARAMETER_INTEGER DOUBLE = ParameterType.PARAMETER_DOUBLE STRING = ParameterType.PARAMETER_STRING + YAML = ParameterType.PARAMETER_YAML BYTE_ARRAY = ParameterType.PARAMETER_BYTE_ARRAY BOOL_ARRAY = ParameterType.PARAMETER_BOOL_ARRAY INTEGER_ARRAY = ParameterType.PARAMETER_INTEGER_ARRAY @@ -97,6 +98,8 @@ def from_parameter_value(cls, return Parameter.Type.DOUBLE elif isinstance(parameter_value, str): return Parameter.Type.STRING + elif isinstance(parameter_value, dict): + return Parameter.Type.YAML elif isinstance(parameter_value, (list, tuple, array.array)): if all(isinstance(v, bytes) for v in parameter_value): return Parameter.Type.BYTE_ARRAY @@ -127,6 +130,8 @@ def check(self, parameter_value: AllowableParameterValue) -> bool: return isinstance(parameter_value, float) if Parameter.Type.STRING == self: return isinstance(parameter_value, str) + if Parameter.Type.YAML == self: + return isinstance(parameter_value, dict) or isinstance(parameter_value, str) if Parameter.Type.BYTE_ARRAY == self: return isinstance(parameter_value, (list, tuple)) and \ all(isinstance(v, bytes) and len(v) == 1 for v in parameter_value) @@ -191,6 +196,14 @@ def __init__(self: Parameter[float], name: str, type_: Literal[Parameter.Type.DO def __init__(self: Parameter[str], name: str, type_: Literal[Parameter.Type.STRING] ) -> None: ... + @overload + def __init__(self: Parameter[dict], name: str, type_: Literal[Parameter.Type.YAML] + ) -> None: ... + + @overload + def __init__(self: Parameter[str], name: str, type_: Literal[Parameter.Type.YAML] + ) -> None: ... + @overload def __init__(self: Parameter[Union[list[bytes], Tuple[bytes, ...]]], name: str, @@ -228,6 +241,15 @@ def __init__(self, name: str, type_: Parameter.Type, value: AllowableParameterValueT) -> None: ... def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None) -> None: + # If is string, try loading it + # If it throws an exception, its not a valid yaml string, so its probably just a normal string + if isinstance(value, str): + try: + value = yaml.safe_load(value) + except: + pass + else: + value = yaml.safe_load(yaml.dump(value)) if type_ is None: # This will raise a TypeError if it is not possible to get a type from the value. type_ = Parameter.Type.from_parameter_value(value) @@ -240,6 +262,9 @@ def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None self._type_ = type_ self._name = name + if type_ == Parameter.Type.YAML: + if isinstance(value, dict): + value = yaml.dump(value) self._value = value @property @@ -253,6 +278,11 @@ def type_(self) -> 'Parameter.Type': @property def value(self) -> AllowableParameterValueT: return self._value + + + def get_yaml_parameter_as_dict(self) -> dict : + assert(self.type_ == Parameter.Type.YAML) + return yaml.safe_load(self.value) def get_parameter_value(self) -> ParameterValue: parameter_value = ParameterValue(type=self.type_.value) @@ -264,6 +294,8 @@ def get_parameter_value(self) -> ParameterValue: parameter_value.double_value = self.value elif Parameter.Type.STRING == self.type_: parameter_value.string_value = self.value + elif Parameter.Type.YAML == self.type_: + parameter_value.yaml_value = self.value elif Parameter.Type.BYTE_ARRAY == self.type_: parameter_value.byte_array_value = self.value elif Parameter.Type.BOOL_ARRAY == self.type_: @@ -292,7 +324,7 @@ def get_parameter_value(string_value: str) -> ParameterValue: yaml_value = yaml.safe_load(string_value) except yaml.parser.ParserError: yaml_value = string_value - + print(type(yaml_value)) if isinstance(yaml_value, bool): value.type = ParameterType.PARAMETER_BOOL value.bool_value = yaml_value @@ -302,6 +334,9 @@ def get_parameter_value(string_value: str) -> ParameterValue: elif isinstance(yaml_value, float): value.type = ParameterType.PARAMETER_DOUBLE value.double_value = yaml_value + elif isinstance(yaml_value, dict): + value.type = ParameterType.PARAMETER_YAML + value.yaml_value = yaml.safe_dump(yaml_value) elif isinstance(yaml_value, list): if all((isinstance(v, bool) for v in yaml_value)): value.type = ParameterType.PARAMETER_BOOL_ARRAY diff --git a/rclpy/src/rclpy/node.cpp b/rclpy/src/rclpy/node.cpp index cfb30e4b0..8fe319dac 100644 --- a/rclpy/src/rclpy/node.cpp +++ b/rclpy/src/rclpy/node.cpp @@ -244,6 +244,9 @@ _parameter_from_rcl_variant( } else if (variant->string_value) { type_enum_value = rcl_interfaces__msg__ParameterType__PARAMETER_STRING; value = py::str(variant->string_value); + } else if (variant->yaml_value) { + type_enum_value = rcl_interfaces__msg__ParameterType__PARAMETER_YAML; + value = py::str(variant->yaml_value); } else if (variant->byte_array_value) { type_enum_value = rcl_interfaces__msg__ParameterType__PARAMETER_BYTE_ARRAY; value = py::bytes( @@ -638,4 +641,4 @@ define_node(py::object module) "get_parameters", &Node::get_parameters, "Get a list of parameters for the current node"); } -} // namespace rclpy +} // namespace rclpy \ No newline at end of file From 920bf83204ef9dc31677bb8fef1c874c225d00f8 Mon Sep 17 00:00:00 2001 From: "Rahul K.A" Date: Thu, 28 Aug 2025 20:37:47 -0400 Subject: [PATCH 2/5] chore: Apply ament uncrustify Signed-off-by: Rahul K.A --- rclpy/src/rclpy/node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rclpy/src/rclpy/node.cpp b/rclpy/src/rclpy/node.cpp index 8fe319dac..2b0df2a23 100644 --- a/rclpy/src/rclpy/node.cpp +++ b/rclpy/src/rclpy/node.cpp @@ -641,4 +641,4 @@ define_node(py::object module) "get_parameters", &Node::get_parameters, "Get a list of parameters for the current node"); } -} // namespace rclpy \ No newline at end of file +} // namespace rclpy From 93e642730ddb8fe54c9e92a8d2a7dcedd911f494 Mon Sep 17 00:00:00 2001 From: "Rahul K.A" Date: Fri, 29 Aug 2025 21:48:54 -0400 Subject: [PATCH 3/5] chore: Add support for setting YAML through cli in rclpy Signed-off-by: Rahul K.A --- rclpy/rclpy/parameter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rclpy/rclpy/parameter.py b/rclpy/rclpy/parameter.py index c35768d60..b1ee99873 100644 --- a/rclpy/rclpy/parameter.py +++ b/rclpy/rclpy/parameter.py @@ -161,6 +161,8 @@ def from_parameter_msg(cls, param_msg: ParameterMsg) -> Parameter[Any]: value = param_msg.value.double_value elif Parameter.Type.STRING == type_: value = param_msg.value.string_value + elif Parameter.Type.YAML == type_: + value = param_msg.value.yaml_value elif Parameter.Type.BYTE_ARRAY == type_: value = param_msg.value.byte_array_value elif Parameter.Type.BOOL_ARRAY == type_: @@ -200,9 +202,6 @@ def __init__(self: Parameter[str], name: str, type_: Literal[Parameter.Type.STRI def __init__(self: Parameter[dict], name: str, type_: Literal[Parameter.Type.YAML] ) -> None: ... - @overload - def __init__(self: Parameter[str], name: str, type_: Literal[Parameter.Type.YAML] - ) -> None: ... @overload def __init__(self: Parameter[Union[list[bytes], Tuple[bytes, ...]]], @@ -241,7 +240,7 @@ def __init__(self, name: str, type_: Parameter.Type, value: AllowableParameterValueT) -> None: ... def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None) -> None: - # If is string, try loading it + # If is string, try loading as a yaml # If it throws an exception, its not a valid yaml string, so its probably just a normal string if isinstance(value, str): try: @@ -249,7 +248,8 @@ def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None except: pass else: - value = yaml.safe_load(yaml.dump(value)) + value = yaml.safe_load(yaml.safe_dump(value)) + if type_ is None: # This will raise a TypeError if it is not possible to get a type from the value. type_ = Parameter.Type.from_parameter_value(value) @@ -262,9 +262,9 @@ def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None self._type_ = type_ self._name = name + if type_ == Parameter.Type.YAML: - if isinstance(value, dict): - value = yaml.dump(value) + value = yaml.safe_dump(value) self._value = value @property From ec70a9f0d94de9bd391bcedd8941bbb8c1f5c813 Mon Sep 17 00:00:00 2001 From: "Rahul K.A" Date: Sat, 30 Aug 2025 14:27:33 -0400 Subject: [PATCH 4/5] chore: Add test for yaml parameters Signed-off-by: Rahul K.A --- rclpy/test/test_parameter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rclpy/test/test_parameter.py b/rclpy/test/test_parameter.py index c0101c9d6..0f9628dd1 100644 --- a/rclpy/test/test_parameter.py +++ b/rclpy/test/test_parameter.py @@ -16,6 +16,7 @@ import os from tempfile import NamedTemporaryFile import unittest +import yaml import pytest from rcl_interfaces.msg import Parameter as ParameterMsg @@ -78,6 +79,16 @@ def test_create_string_parameter(self) -> None: self.assertEqual(p.type_, Parameter.Type.STRING) self.assertEqual(p.value, 'pvalue') + def test_create_yaml_parameter(self)-> None: + yaml_dict = {"a":1, "b":2} + p = Parameter('myparam', Parameter.Type.YAML, yaml_dict) + self.assertEqual(p.name, 'myparam') + self.assertEqual(yaml.safe_load(p.value), yaml_dict) + + p = Parameter('myparam', value = yaml_dict) + self.assertEqual(p.name, 'myparam') + self.assertEqual(yaml.safe_load(p.value), yaml_dict) + def test_create_boolean_array_parameter(self) -> None: p = Parameter('myparam', Parameter.Type.BOOL_ARRAY, [True, False, True]) self.assertEqual(p.value, [True, False, True]) From 9cb9453c7b624a3b241fb6bc235332d2f9a218d7 Mon Sep 17 00:00:00 2001 From: "Rahul K.A" Date: Sun, 31 Aug 2025 11:27:18 -0400 Subject: [PATCH 5/5] chore: Add support for changing individual parameters inside a yaml parameter Signed-off-by: Rahul K.A --- rclpy/rclpy/node.py | 58 +++++++++++++++++++++++++++++++++++++++- rclpy/rclpy/parameter.py | 1 - 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/rclpy/rclpy/node.py b/rclpy/rclpy/node.py index 4e7c05b06..996f5dbe7 100644 --- a/rclpy/rclpy/node.py +++ b/rclpy/rclpy/node.py @@ -14,7 +14,6 @@ import math import time -import yaml from types import TracebackType from typing import Any @@ -1003,6 +1002,7 @@ def _set_parameters_atomically_common( # Descriptors have already been applied by this point. self._parameters[param.name] = param + self._handle_changes_inside_yaml(parameter=param) parameter_event.stamp = self._clock.now().to_msg() if self._parameter_event_publisher: @@ -1013,6 +1013,62 @@ def _set_parameters_atomically_common( return result + + def _traverse_yaml_and_change_value(self, yaml_dict : dict, parameter_path : List[str], parameter_value : ParameterValue) -> None: + """ + Iteratively traverse a nested dictionary to apply a key-value pair + """ + for p in parameter_path[1:-1]: + if (yaml_dict := yaml_dict.get(p)) is None: + break + else: + value_type = parameter_value.type + final_value = None + if Parameter.Type.BOOL == value_type: + final_value = parameter_value.bool_value + elif Parameter.Type.INTEGER == value_type: + final_value = parameter_value.integer_value + elif Parameter.Type.DOUBLE == value_type: + final_value = parameter_value.double_value + elif Parameter.Type.STRING == value_type: + final_value = parameter_value.string_value + elif Parameter.Type.YAML == value_type: + # Setting a nested yaml isnt supported yet + return + elif Parameter.Type.BYTE_ARRAY == value_type: + final_value= parameter_value.byte_array_value + elif Parameter.Type.BOOL_ARRAY == value_type: + final_value = parameter_value.bool_array_value + elif Parameter.Type.INTEGER_ARRAY == value_type: + final_value = parameter_value.integer_array_value + elif Parameter.Type.DOUBLE_ARRAY == value_type: + final_value = parameter_value.double_array_value + elif Parameter.Type.STRING_ARRAY == value_type: + final_value = parameter_value.string_array_value + yaml_dict[parameter_path[-1]] = final_value + return yaml_dict + + + yaml_dict[parameter_path[-1]] = value.get_parameter_value() + + + def _handle_changes_inside_yaml(self, parameter : Parameter) -> None: + name = parameter._name + parameter_path = name.split(".") + # Handle case where the parameter is not splittable into namespaces + if len(parameter_path) == 1 and parameter_path[0] == name: + return + + if parameter_path[0] not in self._parameters.keys(): + return + + if self._parameters[parameter_path[0]]._type_ != Parameter.Type.YAML: + return + yaml_dict = self._parameters[parameter_path[0]].get_yaml_parameter_as_dict() + self._traverse_yaml_and_change_value(yaml_dict, parameter_path, parameter.get_parameter_value()) + self._parameters[parameter_path[0]] = Parameter(name, value=yaml_dict) + + def list_parameters( self, prefixes: List[str], diff --git a/rclpy/rclpy/parameter.py b/rclpy/rclpy/parameter.py index b1ee99873..3b1286642 100644 --- a/rclpy/rclpy/parameter.py +++ b/rclpy/rclpy/parameter.py @@ -324,7 +324,6 @@ def get_parameter_value(string_value: str) -> ParameterValue: yaml_value = yaml.safe_load(string_value) except yaml.parser.ParserError: yaml_value = string_value - print(type(yaml_value)) if isinstance(yaml_value, bool): value.type = ParameterType.PARAMETER_BOOL value.bool_value = yaml_value