Skip to content

Commit cbdb4a8

Browse files
qiayuanlclaude
andcommitted
Use legged_bringup launch utilities for controller setup
- Replace inline generate_temp_config and control_spawner with imports from legged_bringup - Use resolve_policy_paths to absolutize relative policy paths in YAML - Use get_controller_names to auto-discover controllers from YAML - Replace wandb.launch.py include with download_wandb_onnx function call - Add wandb_path launch argument - Remove erroneous use_sim_time=True from real.launch.py - Add --param-file to all spawner calls for Jazzy compliance - Pass robot_type to teleop in real.launch.py and mujoco.launch.py - Remove launch/wandb.launch.py (superseded by download_wandb_onnx) - Add legged_bringup dependency to package.xml Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0dbc3ac commit cbdb4a8

4 files changed

Lines changed: 49 additions & 214 deletions

File tree

launch/mujoco.launch.py

Lines changed: 25 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,34 @@
11
import os
22

3-
import yaml
4-
from ament_index_python.packages import get_package_share_directory
53
from launch import LaunchDescription
64
from launch.actions import (
75
DeclareLaunchArgument,
86
OpaqueFunction,
97
SetLaunchConfiguration,
108
IncludeLaunchDescription
119
)
12-
from launch.conditions import IfCondition
1310
from launch.launch_description_sources import PythonLaunchDescriptionSource
14-
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration, PythonExpression, \
15-
ThisLaunchFileDir
11+
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration, PythonExpression
1612
from launch_ros.actions import Node
1713
from launch_ros.substitutions import FindPackageShare
18-
19-
20-
# --------------------------
21-
# Internal: minimal generic override
22-
# --------------------------
23-
def generate_temp_config(config_path, package_name, kv_pairs):
24-
"""
25-
Load <package_name>/<config_path>, apply overrides from kv_pairs,
26-
and write to /tmp/<package_name>/temp_controllers.yaml. Returns the path.
27-
kv_pairs: list of (dotted_key, raw_value_str)
28-
"""
29-
pkg_dir = get_package_share_directory(package_name)
30-
src_path = os.path.join(pkg_dir, config_path)
31-
dst_path = os.path.join('/tmp', package_name, 'temp_controllers.yaml')
32-
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
33-
34-
with open(src_path, 'r') as f:
35-
cfg = yaml.safe_load(f) or {}
36-
37-
for dotted_key, raw_val in kv_pairs:
38-
parts = [p for p in dotted_key.split('.') if p]
39-
if len(parts) < 2:
40-
raise ValueError(
41-
f"Key '{dotted_key}' is incomplete; expected '<ns>.[ros__parameters.]foo.bar'"
42-
)
43-
# Auto-insert ros__parameters right after namespace if omitted
44-
if parts[1] != 'ros__parameters':
45-
parts.insert(1, 'ros__parameters')
46-
47-
try:
48-
val = yaml.safe_load(raw_val)
49-
except Exception:
50-
val = raw_val
51-
52-
cur = cfg
53-
for k in parts[:-1]:
54-
if not isinstance(cur.get(k), dict):
55-
cur[k] = {}
56-
cur = cur[k]
57-
cur[parts[-1]] = val
58-
59-
with open(dst_path, 'w') as f:
60-
yaml.dump(cfg, f, sort_keys=False)
61-
print(f"[launch] Temp controllers.yaml written to {dst_path}")
62-
63-
return dst_path
64-
65-
66-
# --------------------------
67-
# ROS nodes / launch wiring
68-
# --------------------------
69-
def control_spawner(names, inactive=False):
70-
args = list(names)
71-
if inactive:
72-
args.append('--inactive')
73-
return Node(
74-
package='controller_manager',
75-
executable='spawner',
76-
arguments=args,
77-
output='screen'
78-
)
14+
from legged_bringup.launch_utils import (
15+
get_controller_names, generate_temp_config, resolve_policy_paths, download_wandb_onnx, control_spawner
16+
)
7917

8018

8119
def setup_controllers(context):
8220
robot_type_value = LaunchConfiguration('robot_type').perform(context)
8321
policy_path_value = LaunchConfiguration('policy_path').perform(context)
22+
wandb_path_value = LaunchConfiguration('wandb_path').perform(context)
8423
start_step_value = LaunchConfiguration('start_step').perform(context)
8524
ext_pos_corr = LaunchConfiguration('ext_pos_corr').perform(context)
8625

87-
kv_pairs = []
26+
if not policy_path_value and wandb_path_value:
27+
policy_path_value = download_wandb_onnx(wandb_path_value)
28+
29+
controllers_config_path = f'config/{robot_type_value}/controllers.yaml'
30+
31+
kv_pairs = resolve_policy_paths(controllers_config_path, 'motion_tracking_controller')
8832
if policy_path_value:
8933
abs_path = os.path.abspath(os.path.expanduser(os.path.expandvars(policy_path_value)))
9034
kv_pairs.append(('walking_controller.policy.path', abs_path))
@@ -94,7 +38,6 @@ def setup_controllers(context):
9438
kv_pairs.append(('state_estimator.estimation.contact.height_sensor_noise', 1e10))
9539
kv_pairs.append(('state_estimator.estimation.position.topic', "/mid360"))
9640

97-
controllers_config_path = f'config/{robot_type_value}/controllers.yaml'
9841
temp_controllers_config_path = generate_temp_config(
9942
controllers_config_path,
10043
'motion_tracking_controller',
@@ -106,11 +49,13 @@ def setup_controllers(context):
10649
value=temp_controllers_config_path
10750
)
10851

52+
all_controllers = get_controller_names(controllers_config_path, 'motion_tracking_controller')
10953
active_list = ["state_estimator", "walking_controller"]
110-
inactive_list = ["standby_controller"]
54+
inactive_list = [c for c in all_controllers if c not in active_list]
11155

112-
active_spawner = control_spawner(active_list)
113-
inactive_spawner = control_spawner(inactive_list, inactive=True)
56+
param_file = LaunchConfiguration('controllers_yaml')
57+
active_spawner = control_spawner(active_list, param_file=param_file)
58+
inactive_spawner = control_spawner(inactive_list, param_file=param_file, inactive=True)
11459

11560
return [set_controllers_yaml, active_spawner, inactive_spawner]
11661

@@ -156,16 +101,6 @@ def generate_launch_description():
156101
],
157102
output='screen')
158103

159-
wandb = IncludeLaunchDescription(
160-
PythonLaunchDescriptionSource([ThisLaunchFileDir(), "/wandb.launch.py"]),
161-
launch_arguments={
162-
"wandb_path": LaunchConfiguration("wandb_path")
163-
}.items(),
164-
condition=IfCondition(
165-
PythonExpression(["'", LaunchConfiguration('policy_path'), "' == ''"])
166-
)
167-
)
168-
169104
controllers_opaque_func = OpaqueFunction(function=setup_controllers)
170105

171106
teleop = PathJoinSubstitution([
@@ -191,9 +126,16 @@ def generate_launch_description():
191126
default_value='false',
192127
description='Enable external position correction'
193128
),
194-
wandb,
129+
DeclareLaunchArgument(
130+
'wandb_path',
131+
default_value='',
132+
description='W&B run path to download ONNX from (used when policy_path is empty)'
133+
),
195134
controllers_opaque_func,
196135
mujoco_simulator,
197136
node_robot_state_publisher,
198-
IncludeLaunchDescription(PythonLaunchDescriptionSource(teleop))
137+
IncludeLaunchDescription(
138+
PythonLaunchDescriptionSource(teleop),
139+
launch_arguments={'robot_type': robot_type}.items()
140+
)
199141
])

launch/real.launch.py

Lines changed: 23 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import os
22

3-
import yaml
4-
from ament_index_python.packages import get_package_share_directory
53
from launch import LaunchDescription
64
from launch.actions import (
75
ExecuteProcess,
@@ -10,83 +8,28 @@
108
SetLaunchConfiguration,
119
IncludeLaunchDescription,
1210
)
13-
from launch.conditions import IfCondition
1411
from launch.launch_description_sources import PythonLaunchDescriptionSource
15-
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration, PythonExpression, \
16-
ThisLaunchFileDir
12+
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration, PythonExpression
1713
from launch_ros.actions import Node
1814
from launch_ros.substitutions import FindPackageShare
19-
20-
21-
# --------------------------
22-
# Internal: minimal generic override
23-
# --------------------------
24-
def generate_temp_config(config_path, package_name, kv_pairs):
25-
"""
26-
Load <package_name>/<config_path>, apply overrides from kv_pairs,
27-
and write to /tmp/<package_name>/temp_controllers.yaml. Returns the path.
28-
kv_pairs: list of (dotted_key, raw_value_str)
29-
"""
30-
pkg_dir = get_package_share_directory(package_name)
31-
src_path = os.path.join(pkg_dir, config_path)
32-
dst_path = os.path.join('/tmp', package_name, 'temp_controllers.yaml')
33-
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
34-
35-
with open(src_path, 'r') as f:
36-
cfg = yaml.safe_load(f) or {}
37-
38-
for dotted_key, raw_val in kv_pairs:
39-
parts = [p for p in dotted_key.split('.') if p]
40-
if len(parts) < 2:
41-
raise ValueError(
42-
f"Key '{dotted_key}' is incomplete; expected '<ns>.[ros__parameters.]foo.bar'"
43-
)
44-
# Auto-insert ros__parameters right after namespace if omitted
45-
if parts[1] != 'ros__parameters':
46-
parts.insert(1, 'ros__parameters')
47-
48-
try:
49-
val = yaml.safe_load(raw_val)
50-
except Exception:
51-
val = raw_val
52-
53-
cur = cfg
54-
for k in parts[:-1]:
55-
if not isinstance(cur.get(k), dict):
56-
cur[k] = {}
57-
cur = cur[k]
58-
cur[parts[-1]] = val
59-
60-
with open(dst_path, 'w') as f:
61-
yaml.dump(cfg, f, sort_keys=False)
62-
print(f"[launch] Temp controllers.yaml written to {dst_path}")
63-
64-
return dst_path
65-
66-
67-
# --------------------------
68-
# ROS nodes / launch wiring
69-
# --------------------------
70-
def control_spawner(names, inactive=False):
71-
args = list(names)
72-
args += ['--param-file', LaunchConfiguration('controllers_yaml')]
73-
if inactive:
74-
args.append('--inactive')
75-
return Node(
76-
package='controller_manager',
77-
executable='spawner',
78-
arguments=args,
79-
output='screen'
80-
)
15+
from legged_bringup.launch_utils import (
16+
get_controller_names, generate_temp_config, resolve_policy_paths, download_wandb_onnx, control_spawner
17+
)
8118

8219

8320
def setup_controllers(context):
8421
robot_type_value = LaunchConfiguration('robot_type').perform(context)
8522
policy_path_value = LaunchConfiguration('policy_path').perform(context)
23+
wandb_path_value = LaunchConfiguration('wandb_path').perform(context)
8624
start_step_value = LaunchConfiguration('start_step').perform(context)
8725
ext_pos_corr = LaunchConfiguration('ext_pos_corr').perform(context)
8826

89-
kv_pairs = []
27+
if not policy_path_value and wandb_path_value:
28+
policy_path_value = download_wandb_onnx(wandb_path_value)
29+
30+
controllers_config_path = f'config/{robot_type_value}/controllers.yaml'
31+
32+
kv_pairs = resolve_policy_paths(controllers_config_path, 'motion_tracking_controller')
9033
if policy_path_value:
9134
abs_path = os.path.abspath(os.path.expanduser(os.path.expandvars(policy_path_value)))
9235
kv_pairs.append(('walking_controller.policy.path', abs_path))
@@ -96,7 +39,6 @@ def setup_controllers(context):
9639
kv_pairs.append(('state_estimator.estimation.contact.height_sensor_noise', 1e10))
9740
kv_pairs.append(('state_estimator.estimation.position.topic', "/glim/odom"))
9841

99-
controllers_config_path = f'config/{robot_type_value}/controllers.yaml'
10042
temp_controllers_config_path = generate_temp_config(
10143
controllers_config_path,
10244
'motion_tracking_controller',
@@ -108,11 +50,13 @@ def setup_controllers(context):
10850
value=temp_controllers_config_path
10951
)
11052

53+
all_controllers = get_controller_names(controllers_config_path, 'motion_tracking_controller')
11154
active_list = ["state_estimator", "standby_controller"]
112-
inactive_list = ["walking_controller"]
55+
inactive_list = [c for c in all_controllers if c not in active_list]
11356

114-
active_spawner = control_spawner(active_list)
115-
inactive_spawner = control_spawner(inactive_list, inactive=True)
57+
param_file = LaunchConfiguration('controllers_yaml')
58+
active_spawner = control_spawner(active_list, param_file=param_file)
59+
inactive_spawner = control_spawner(inactive_list, inactive=True, param_file=param_file)
11660

11761
return [set_controllers_yaml, active_spawner, inactive_spawner]
11862

@@ -144,7 +88,6 @@ def generate_launch_description():
14488
output='screen',
14589
parameters=[robot_description, {
14690
'publish_frequency': 500.0,
147-
'use_sim_time': True
14891
}],
14992
)
15093

@@ -156,16 +99,6 @@ def generate_launch_description():
15699
respawn=True,
157100
)
158101

159-
wandb = IncludeLaunchDescription(
160-
PythonLaunchDescriptionSource([ThisLaunchFileDir(), "/wandb.launch.py"]),
161-
launch_arguments={
162-
"wandb_path": LaunchConfiguration("wandb_path")
163-
}.items(),
164-
condition=IfCondition(
165-
PythonExpression(["'", LaunchConfiguration('policy_path'), "' == ''"])
166-
)
167-
)
168-
169102
controllers_opaque_func = OpaqueFunction(function=setup_controllers)
170103

171104
# Exclude all Unitree topics... it should start from the same namespace, fuck Unitree!
@@ -220,12 +153,17 @@ def generate_launch_description():
220153
default_value='false',
221154
description='Enable external position correction'
222155
),
223-
wandb,
156+
DeclareLaunchArgument(
157+
'wandb_path',
158+
default_value='',
159+
description='W&B run path to download ONNX from (used when policy_path is empty)'
160+
),
224161
controllers_opaque_func,
225162
control_node,
226163
node_robot_state_publisher,
227164
rosbag2,
228165
IncludeLaunchDescription(
229-
PythonLaunchDescriptionSource(teleop)
166+
PythonLaunchDescriptionSource(teleop),
167+
launch_arguments={'robot_type': robot_type}.items()
230168
)
231169
])

launch/wandb.launch.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

package.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<build_depend>ament_cmake_auto</build_depend>
1212

1313
<depend>legged_rl_controllers</depend>
14+
<depend>legged_bringup</depend>
1415
<depend>rosbag2-storage-mcap</depend>
1516

1617
<export>

0 commit comments

Comments
 (0)