Skip to content

Commit

Permalink
Fix Hardware spawner and add tests for it (backport #1759) (#1827)
Browse files Browse the repository at this point in the history
* Fix Hardware spawner and add tests for it (#1759)

* Tests for hardware spawner.

* Fully support spawning a list of hardware components

* Use python3 coverage instead of ros2 run in tests

* Actually check component's state after changing it

* Update hardware_spawner's user documentation

---------

Co-authored-by: Dr. Denis Štogl <[email protected]>
Co-authored-by: Bence Magyar <[email protected]>
Co-authored-by: Christoph Fröhlich <[email protected]>
(cherry picked from commit 5dbd6d5)

# Conflicts:
#	controller_manager/CMakeLists.txt

* Fix conflicts

* FIx missing lines

* Fix the missing bcolors.ENDC in hardware_spawner log prints (#1870)

* Fix pre-commit

* Fix the API of the tests and the CMakeLists.txt

* Apply target properties as other tests

---------

Co-authored-by: Felix Exner (fexner) <[email protected]>
Co-authored-by: Sai Kishor Kothakota <[email protected]>
Co-authored-by: Christoph Froehlich <[email protected]>
  • Loading branch information
4 people authored Nov 10, 2024
1 parent 0cb0716 commit dcb958f
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 45 deletions.
8 changes: 8 additions & 0 deletions controller_manager/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ if(BUILD_TESTING)
target_link_libraries(test_spawner_unspawner ${PROJECT_NAME} test_controller)
ament_target_dependencies(test_spawner_unspawner ros2_control_test_assets)

ament_add_gmock(test_hardware_spawner
test/test_hardware_spawner.cpp
TIMEOUT 120
)
target_include_directories(test_hardware_spawner PRIVATE include)
target_link_libraries(test_hardware_spawner ${PROJECT_NAME})
ament_target_dependencies(test_hardware_spawner ros2_control_test_assets)

install(FILES test/test_controller_spawner_with_type.yaml
DESTINATION test)

Expand Down
79 changes: 37 additions & 42 deletions controller_manager/controller_manager/hardware_spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def has_service_names(node, node_name, node_namespace, service_names):
def is_hardware_component_loaded(
node, controller_manager, hardware_component, service_timeout=0.0
):
components = list_hardware_components(node, hardware_component, service_timeout).component
components = list_hardware_components(node, controller_manager, service_timeout).component
return any(c.name == hardware_component for c in components)


Expand All @@ -67,49 +67,45 @@ def handle_set_component_state_service_call(
response = set_hardware_component_state(node, controller_manager_name, component, target_state)
if response.ok and response.state == target_state:
node.get_logger().info(
bcolors.OKGREEN
+ f"{action} component '{component}'. Hardware now in state: {response.state}."
f"{bcolors.OKGREEN}{action} component '{component}'. Hardware now in state: {response.state}.{bcolors.ENDC}"
)
elif response.ok and not response.state == target_state:
node.get_logger().warn(
bcolors.WARNING
+ f"Could not {action} component '{component}'. Service call returned ok=True, but state: {response.state} is not equal to target state '{target_state}'."
f"{bcolors.WARNING}Could not {action} component '{component}'. Service call returned ok=True, but state: {response.state} is not equal to target state '{target_state}'.{bcolors.ENDC}"
)
else:
node.get_logger().warn(
bcolors.WARNING
+ f"Could not {action} component '{component}'. Service call failed. Wrong component name?"
f"{bcolors.WARNING}Could not {action} component '{component}'. Service call failed. Wrong component name?{bcolors.ENDC}"
)


def activate_components(node, controller_manager_name, components_to_activate):
def activate_component(node, controller_manager_name, component_to_activate):
active_state = State()
active_state.id = State.PRIMARY_STATE_ACTIVE
active_state.label = "active"
for component in components_to_activate:
handle_set_component_state_service_call(
node, controller_manager_name, component, active_state, "activated"
)
handle_set_component_state_service_call(
node, controller_manager_name, component_to_activate, active_state, "activated"
)


def configure_components(node, controller_manager_name, components_to_configure):
def configure_component(node, controller_manager_name, component_to_configure):
inactive_state = State()
inactive_state.id = State.PRIMARY_STATE_INACTIVE
inactive_state.label = "inactive"
for component in components_to_configure:
handle_set_component_state_service_call(
node, controller_manager_name, component, inactive_state, "configured"
)
handle_set_component_state_service_call(
node, controller_manager_name, component_to_configure, inactive_state, "configured"
)


def main(args=None):
rclpy.init(args=args, signal_handler_options=SignalHandlerOptions.NO)
parser = argparse.ArgumentParser()
activate_or_confiigure_grp = parser.add_mutually_exclusive_group(required=True)
activate_or_configure_grp = parser.add_mutually_exclusive_group(required=True)

parser.add_argument(
"hardware_component_name",
help="The name of the hardware component which should be activated.",
"hardware_component_names",
help="The name of the hardware components which should be activated.",
nargs="+",
)
parser.add_argument(
"-c",
Expand All @@ -126,13 +122,13 @@ def main(args=None):
type=float,
)
# add arguments which are mutually exclusive
activate_or_confiigure_grp.add_argument(
activate_or_configure_grp.add_argument(
"--activate",
help="Activates the given components. Note: Components are by default configured before activated. ",
action="store_true",
required=False,
)
activate_or_confiigure_grp.add_argument(
activate_or_configure_grp.add_argument(
"--configure",
help="Configures the given components.",
action="store_true",
Expand All @@ -141,9 +137,9 @@ def main(args=None):

command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
args = parser.parse_args(command_line_args)
hardware_components = args.hardware_component_names
controller_manager_name = args.controller_manager
controller_manager_timeout = args.controller_manager_timeout
hardware_component = [args.hardware_component_name]
activate = args.activate
configure = args.configure

Expand All @@ -156,28 +152,27 @@ def main(args=None):
controller_manager_name = f"/{controller_manager_name}"

try:
if not is_hardware_component_loaded(
node, controller_manager_name, hardware_component, controller_manager_timeout
):
node.get_logger().warn(
bcolors.WARNING
+ "Hardware Component is not loaded - state can not be changed."
+ bcolors.ENDC
)
elif activate:
activate_components(node, controller_manager_name, hardware_component)
elif configure:
configure_components(node, controller_manager_name, hardware_component)
else:
node.get_logger().error(
'You need to either specify if the hardware component should be activated with the "--activate" flag or configured with the "--configure" flag'
)
parser.print_help()
return 0
for hardware_component in hardware_components:
if not is_hardware_component_loaded(
node, controller_manager_name, hardware_component, controller_manager_timeout
):
node.get_logger().warn(
f"{bcolors.WARNING}Hardware Component is not loaded - state can not be changed.{bcolors.ENDC}"
)
elif activate:
activate_component(node, controller_manager_name, hardware_component)
elif configure:
configure_component(node, controller_manager_name, hardware_component)
else:
node.get_logger().error(
f'{bcolors.FAIL}You need to either specify if the hardware component should be activated with the "--activate" flag or configured with the "--configure" flag{bcolors.ENDC}'
)
parser.print_help()
return 0
except KeyboardInterrupt:
pass
except ServiceNotFoundError as err:
node.get_logger().fatal(str(err))
node.get_logger().fatal(f"{bcolors.FAIL}{str(err)}{bcolors.ENDC}")
return 1
finally:
rclpy.shutdown()
Expand Down
10 changes: 7 additions & 3 deletions controller_manager/doc/userdoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,20 @@ The parsed controller config file can follow the same conventions as the typical
.. code-block:: console
$ ros2 run controller_manager hardware_spawner -h
usage: hardware_spawner [-h] [-c CONTROLLER_MANAGER] (--activate | --configure) hardware_component_name
usage: hardware_spawner [-h] [-c CONTROLLER_MANAGER] [--controller-manager-timeout CONTROLLER_MANAGER_TIMEOUT]
(--activate | --configure)
hardware_component_names [hardware_component_names ...]
positional arguments:
hardware_component_name
The name of the hardware component which should be activated.
hardware_component_names
The name of the hardware components which should be activated.
options:
-h, --help show this help message and exit
-c CONTROLLER_MANAGER, --controller-manager CONTROLLER_MANAGER
Name of the controller manager ROS node
--controller-manager-timeout CONTROLLER_MANAGER_TIMEOUT
Time to wait for the controller manager
--activate Activates the given components. Note: Components are by default configured before activated.
--configure Configures the given components.
Expand Down
Loading

0 comments on commit dcb958f

Please sign in to comment.