Skip to content

Commit 06fae31

Browse files
authored
Merge branch 'main' into base-link
2 parents 5fead2c + aafaea0 commit 06fae31

59 files changed

Lines changed: 2307 additions & 529 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
build/
22
install/
33
log/
4+
.vscode/
5+
46
# Byte-compiled / optimized / DLL files
57
__pycache__/
68
*.py[cod]

‎.vscode/settings.json‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"files.associations": {
3+
"cctype": "cpp",
4+
"clocale": "cpp",
5+
"cmath": "cpp",
6+
"cstdarg": "cpp",
7+
"cstddef": "cpp",
8+
"cstdio": "cpp",
9+
"cstdlib": "cpp",
10+
"cstring": "cpp",
11+
"ctime": "cpp",
12+
"cwchar": "cpp",
13+
"cwctype": "cpp",
14+
"array": "cpp",
15+
"atomic": "cpp",
16+
"bit": "cpp",
17+
"bitset": "cpp",
18+
"chrono": "cpp",
19+
"compare": "cpp",
20+
"complex": "cpp",
21+
"concepts": "cpp",
22+
"condition_variable": "cpp",
23+
"cstdint": "cpp",
24+
"deque": "cpp",
25+
"forward_list": "cpp",
26+
"list": "cpp",
27+
"map": "cpp",
28+
"set": "cpp",
29+
"string": "cpp",
30+
"unordered_map": "cpp",
31+
"unordered_set": "cpp",
32+
"vector": "cpp",
33+
"exception": "cpp",
34+
"algorithm": "cpp",
35+
"functional": "cpp",
36+
"iterator": "cpp",
37+
"memory": "cpp",
38+
"memory_resource": "cpp",
39+
"numeric": "cpp",
40+
"optional": "cpp",
41+
"random": "cpp",
42+
"ratio": "cpp",
43+
"string_view": "cpp",
44+
"system_error": "cpp",
45+
"tuple": "cpp",
46+
"type_traits": "cpp",
47+
"utility": "cpp",
48+
"fstream": "cpp",
49+
"initializer_list": "cpp",
50+
"iomanip": "cpp",
51+
"iosfwd": "cpp",
52+
"iostream": "cpp",
53+
"istream": "cpp",
54+
"limits": "cpp",
55+
"mutex": "cpp",
56+
"new": "cpp",
57+
"numbers": "cpp",
58+
"ostream": "cpp",
59+
"semaphore": "cpp",
60+
"sstream": "cpp",
61+
"stdexcept": "cpp",
62+
"stop_token": "cpp",
63+
"streambuf": "cpp",
64+
"thread": "cpp",
65+
"cinttypes": "cpp",
66+
"typeindex": "cpp",
67+
"typeinfo": "cpp"
68+
}
69+
}

‎src/bringup/CMakeLists.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/config)
1616
install(DIRECTORY config DESTINATION share/${PROJECT_NAME}/)
1717
endif()
1818

19+
# Install controller_switcher script from bringup package if present
20+
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/scripts/controller_switcher.py)
21+
install(PROGRAMS
22+
scripts/controller_switcher.py
23+
DESTINATION lib/${PROJECT_NAME}
24+
)
25+
endif()
26+
1927
ament_package()

‎src/bringup/package.xml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99

1010
<buildtool_depend>ament_cmake</buildtool_depend>
1111

12+
<build_depend>msgs</build_depend>
13+
<exec_depend>msgs</exec_depend>
14+
<exec_depend>rclpy</exec_depend>
15+
<exec_depend>controller_manager_msgs</exec_depend>
16+
1217
<exec_depend>arm_bringup</exec_depend>
1318
<exec_depend>drive_bringup</exec_depend>
1419
<exec_depend>science_bringup</exec_depend>
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env python3
2+
import rclpy
3+
from rclpy.node import Node
4+
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
5+
from rclpy.executors import MultiThreadedExecutor
6+
from rclpy.duration import Duration
7+
import threading
8+
9+
from msgs.srv import SetController
10+
from controller_manager_msgs.srv import SwitchController, ListControllers
11+
12+
13+
class ControllerSwitcher(Node):
14+
def __init__(self):
15+
super().__init__('controller_switcher')
16+
17+
# Create separate callback groups
18+
self.service_cb_group = ReentrantCallbackGroup()
19+
self.client_cb_group = MutuallyExclusiveCallbackGroup()
20+
21+
# Controllers to always keep active or ignore
22+
self.always_active = ["joint_state_broadcaster"]
23+
self.ignore_controllers = []
24+
25+
# Lock to prevent concurrent service processing
26+
self.service_lock = threading.Lock()
27+
28+
# Create service with callback group
29+
self.srv = self.create_service(
30+
SetController,
31+
'set_controller',
32+
self.set_controller_callback,
33+
callback_group=self.service_cb_group
34+
)
35+
36+
# Create clients with separate callback group
37+
self.switch_client = self.create_client(
38+
SwitchController,
39+
'/controller_manager/switch_controller',
40+
callback_group=self.client_cb_group
41+
)
42+
self.list_client = self.create_client(
43+
ListControllers,
44+
'/controller_manager/list_controllers',
45+
callback_group=self.client_cb_group
46+
)
47+
48+
# Wait for services with timeout
49+
self.get_logger().info('Waiting for controller_manager services...')
50+
services_ready = False
51+
for _ in range(30): # Try for 30 seconds
52+
if (self.switch_client.wait_for_service(timeout_sec=1.0) and
53+
self.list_client.wait_for_service(timeout_sec=1.0)):
54+
services_ready = True
55+
break
56+
self.get_logger().warn('Still waiting for controller_manager services...')
57+
58+
if not services_ready:
59+
self.get_logger().error('Controller manager services not available!')
60+
raise RuntimeError('Controller manager services timeout')
61+
62+
self.get_logger().info('ControllerSwitcher ready!')
63+
64+
def set_controller_callback(self, request, response):
65+
# Use lock to ensure only one service call is processed at a time
66+
with self.service_lock:
67+
return self._handle_set_controller(request, response)
68+
69+
def _handle_set_controller(self, request, response):
70+
try:
71+
requested_controllers = list(request.controller_names) if request.controller_names else []
72+
73+
# Empty list means deactivate all (except always_active and ignored)
74+
if not requested_controllers:
75+
self.get_logger().info("Empty controller list - will deactivate all except protected controllers")
76+
else:
77+
self.get_logger().info(f"Received request to switch to: {requested_controllers}")
78+
79+
# Get list of all controllers
80+
list_req = ListControllers.Request()
81+
self.get_logger().debug("Calling list_controllers service...")
82+
83+
# Call and wait for response
84+
try:
85+
list_result = self.list_client.call(list_req)
86+
except Exception as e:
87+
response.success = False
88+
response.message = f"Failed to call list_controllers: {str(e)}"
89+
return response
90+
91+
if list_result is None:
92+
response.success = False
93+
response.message = "Failed to get controller list"
94+
return response
95+
96+
# Extract controller names and their states
97+
all_controllers = []
98+
active_controllers = []
99+
for c in list_result.controller:
100+
all_controllers.append(c.name)
101+
if c.state == 'active':
102+
active_controllers.append(c.name)
103+
104+
self.get_logger().info(f"Available controllers: {all_controllers}")
105+
self.get_logger().info(f"Currently active: {active_controllers}")
106+
107+
# Check if all requested controllers exist (only if not empty)
108+
if requested_controllers:
109+
missing_controllers = []
110+
for controller in requested_controllers:
111+
if controller not in all_controllers:
112+
missing_controllers.append(controller)
113+
114+
if missing_controllers:
115+
response.success = False
116+
response.message = f"Controllers not found: {missing_controllers}. Available: {all_controllers}"
117+
return response
118+
119+
# Build lists for activation/deactivation
120+
controllers_to_activate = []
121+
122+
# If not empty, add requested controllers that aren't already active
123+
if requested_controllers:
124+
for controller in requested_controllers:
125+
if controller not in active_controllers:
126+
controllers_to_activate.append(controller)
127+
128+
# Always ensure joint_state_broadcaster is active
129+
if "joint_state_broadcaster" not in active_controllers and "joint_state_broadcaster" not in controllers_to_activate:
130+
controllers_to_activate.append("joint_state_broadcaster")
131+
132+
# Deactivate logic:
133+
# - If empty list: deactivate all except always_active and ignored
134+
# - If controllers specified: deactivate all except requested, always_active, and ignored
135+
controllers_to_deactivate = []
136+
for controller in active_controllers:
137+
if controller in self.always_active or controller in self.ignore_controllers:
138+
continue # Skip protected controllers
139+
140+
if not requested_controllers or controller not in requested_controllers:
141+
controllers_to_deactivate.append(controller)
142+
143+
# Check if already in desired state
144+
if requested_controllers:
145+
all_requested_active = all(c in active_controllers for c in requested_controllers)
146+
if all_requested_active and len(controllers_to_deactivate) == 0:
147+
response.success = True
148+
response.message = f"Controllers {requested_controllers} already active with correct configuration"
149+
self.get_logger().info("Already in desired state")
150+
return response
151+
else:
152+
# For empty list, check if only protected controllers are active
153+
if len(controllers_to_deactivate) == 0 and len(controllers_to_activate) == 0:
154+
response.success = True
155+
response.message = "Only protected controllers are active"
156+
self.get_logger().info("Already in desired state")
157+
return response
158+
159+
self.get_logger().info(f"Will activate: {controllers_to_activate}")
160+
self.get_logger().info(f"Will deactivate: {controllers_to_deactivate}")
161+
162+
# Skip if nothing to do
163+
if not controllers_to_activate and not controllers_to_deactivate:
164+
response.success = True
165+
response.message = "No changes needed"
166+
return response
167+
168+
# Perform the switch
169+
switch_req = SwitchController.Request()
170+
switch_req.activate_controllers = controllers_to_activate
171+
switch_req.deactivate_controllers = controllers_to_deactivate
172+
switch_req.strictness = SwitchController.Request.BEST_EFFORT
173+
switch_req.start_asap = True
174+
switch_req.timeout = Duration(seconds=10).to_msg()
175+
176+
self.get_logger().debug("Calling switch_controller service...")
177+
178+
try:
179+
switch_result = self.switch_client.call(switch_req)
180+
except Exception as e:
181+
response.success = False
182+
response.message = f"Failed to call switch_controller: {str(e)}"
183+
return response
184+
185+
if switch_result and switch_result.ok:
186+
response.success = True
187+
if requested_controllers:
188+
active_list = requested_controllers + self.always_active
189+
response.message = f"Successfully activated controllers: {requested_controllers}. Active: {active_list}"
190+
else:
191+
response.message = f"Successfully deactivated all controllers except: {self.always_active + self.ignore_controllers}"
192+
self.get_logger().info(f"Success! {response.message}")
193+
else:
194+
response.success = False
195+
response.message = "Controller switch failed"
196+
self.get_logger().error("Switch failed")
197+
198+
except Exception as e:
199+
self.get_logger().error(f"Error in set_controller_callback: {str(e)}")
200+
import traceback
201+
self.get_logger().error(traceback.format_exc())
202+
response.success = False
203+
response.message = f"Exception: {str(e)}"
204+
205+
self.get_logger().info(f"Returning response: success={response.success}, message={response.message}")
206+
return response
207+
208+
209+
def main(args=None):
210+
rclpy.init(args=args)
211+
212+
try:
213+
node = ControllerSwitcher()
214+
# Use MultiThreadedExecutor with more threads
215+
executor = MultiThreadedExecutor(num_threads=4)
216+
executor.add_node(node)
217+
218+
try:
219+
executor.spin()
220+
finally:
221+
executor.shutdown()
222+
node.destroy_node()
223+
224+
except Exception as e:
225+
print(f"Failed to start ControllerSwitcher: {e}")
226+
finally:
227+
rclpy.shutdown()
228+
229+
230+
if __name__ == '__main__':
231+
main()

0 commit comments

Comments
 (0)