Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion ros2controlcli/doc/userdoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ unload_controller
.. code-block:: console

$ ros2 control unload_controller -h
usage: ros2 control unload_controller [-h] [--spin-time SPIN_TIME] [-s] [-c CONTROLLER_MANAGER] [--include-hidden-nodes] [--ros-args ...] controller_name
usage: ros2 control unload_controller [-h] [--spin-time SPIN_TIME] [-s] [--all-inactive] [-c CONTROLLER_MANAGER] [--include-hidden-nodes] [--ros-args ...] [controller_name]

Unload a controller in a controller manager

Expand All @@ -340,12 +340,27 @@ unload_controller
--spin-time SPIN_TIME
Spin time in seconds to wait for discovery (only applies when not using an already running daemon)
-s, --use-sim-time Enable ROS simulation time
--all-inactive Unload all controllers currently in the 'inactive' state
-c CONTROLLER_MANAGER, --controller-manager CONTROLLER_MANAGER
Name of the controller manager ROS node (default: controller_manager)
--include-hidden-nodes
Consider hidden nodes as well
--ros-args ... Pass arbitrary arguments to the executable

Either ``controller_name`` or ``--all-inactive`` must be given, but not both.

Example unloading a single controller:

.. code-block:: console

$ ros2 control unload_controller test_controller_name

Example unloading every currently inactive controller:

.. code-block:: console

$ ros2 control unload_controller --all-inactive

cleanup_controller
----------------------

Expand Down
54 changes: 52 additions & 2 deletions ros2controlcli/ros2controlcli/verb/unload_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from controller_manager import unload_controller, bcolors
from controller_manager import list_controllers, unload_controller, bcolors

from ros2cli.node.direct import add_arguments
from ros2cli.node.strategy import NodeStrategy
Expand All @@ -21,17 +21,67 @@
from ros2controlcli.api import add_controller_mgr_parsers, LoadedControllerNameCompleter


def get_inactive_controller_names(controllers):
"""
Return the names of controllers currently in the 'inactive' state.

:return: list of controller name strings whose state == "inactive"
"""
return [c.name for c in controllers if c.state == "inactive"]


class UnloadControllerVerb(VerbExtension):
"""Unload a controller in a controller manager."""

def add_arguments(self, parser, cli_name):
add_arguments(parser)
arg = parser.add_argument("controller_name", help="Name of the controller")
arg = parser.add_argument(
"controller_name", nargs="?", default=None, help="Name of the controller"
)
arg.completer = LoadedControllerNameCompleter()
parser.add_argument(
"--all-inactive",
action="store_true",
help="Unload all controllers currently in the 'inactive' state",
)
add_controller_mgr_parsers(parser)

def main(self, *, args):
if args.all_inactive and args.controller_name is not None:
print(
f"{bcolors.FAIL}Cannot use --all-inactive together with a controller_name{bcolors.ENDC}"
)
return 1

if not args.all_inactive and args.controller_name is None:
print(
f"{bcolors.FAIL}Either controller_name or --all-inactive is required{bcolors.ENDC}"
)
return 1

with NodeStrategy(args).direct_node as node:
if args.all_inactive:
controllers = list_controllers(node, args.controller_manager).controller
controller_names = get_inactive_controller_names(controllers)
if not controller_names:
print(f"{bcolors.OKBLUE}No inactive controllers to unload{bcolors.ENDC}")
return 0

any_failed = False
for controller_name in controller_names:
response = unload_controller(node, args.controller_manager, controller_name)
if not response.ok:
any_failed = True
print(
f"{bcolors.FAIL}Error unloading controller {controller_name}, check controller_manager logs{bcolors.ENDC}"
)
continue

print(
f"{bcolors.OKBLUE}Successfully unloaded controller {controller_name}{bcolors.ENDC}"
)
return 1 if any_failed else 0

response = unload_controller(node, args.controller_manager, args.controller_name)
if not response.ok:
print(
Expand Down
46 changes: 46 additions & 0 deletions ros2controlcli/test/test_unload_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2026 PAL Robotics S.L.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest

from controller_manager_msgs.msg import ControllerState

from ros2controlcli.verb.unload_controller import get_inactive_controller_names


class TestUnloadController(unittest.TestCase):
def test_filters_only_inactive(self):
controllers = [
ControllerState(name="ctrl_active", state="active"),
ControllerState(name="ctrl_inactive_1", state="inactive"),
ControllerState(name="ctrl_unconfigured", state="unconfigured"),
ControllerState(name="ctrl_inactive_2", state="inactive"),
]
self.assertEqual(
get_inactive_controller_names(controllers), ["ctrl_inactive_1", "ctrl_inactive_2"]
)

def test_no_inactive_returns_empty(self):
controllers = [
ControllerState(name="ctrl_active", state="active"),
ControllerState(name="ctrl_unconfigured", state="unconfigured"),
]
self.assertEqual(get_inactive_controller_names(controllers), [])

def test_all_inactive(self):
controllers = [
ControllerState(name="ctrl_1", state="inactive"),
ControllerState(name="ctrl_2", state="inactive"),
]
self.assertEqual(get_inactive_controller_names(controllers), ["ctrl_1", "ctrl_2"])
Loading