From e152176b854b3a71948ba56218079d9b971e1cbe Mon Sep 17 00:00:00 2001
From: li-strix <128799116+stevenaya@users.noreply.github.com>
Date: Sun, 1 Nov 2026 06:11:10 +0900
Subject: [PATCH 1/4] Generate shared control interfaces from enabled
components
---
assets/common/ros2_control.xacro | 123 ++++++++++++++
.../robot_presets/default_bimanual.yaml | 4 +
.../config/robot_presets/left_arm.yaml | 4 +
.../left_arm_with_pinch_gripper.yaml | 4 +
.../config/robot_presets/right_arm.yaml | 4 +
.../right_arm_with_pinch_gripper.yaml | 4 +
.../robot/openarm_v2.0/urdf/example/v2.urdf | 16 --
.../openarm_v2.0/urdf/openarm_v20.urdf.xacro | 27 +--
.../openarm.bimanual.ros2_control.xacro | 157 ------------------
.../ros2_control/openarm.ros2_control.xacro | 107 ------------
launch/display_openarm.launch.py | 5 +-
test/support.py | 11 --
test/test_interfaces.py | 56 +++++++
test/test_models.py | 43 +++++
14 files changed, 254 insertions(+), 311 deletions(-)
create mode 100644 assets/common/ros2_control.xacro
delete mode 100644 assets/robot/openarm_v2.0/urdf/ros2_control/openarm.bimanual.ros2_control.xacro
delete mode 100644 assets/robot/openarm_v2.0/urdf/ros2_control/openarm.ros2_control.xacro
create mode 100644 test/test_models.py
diff --git a/assets/common/ros2_control.xacro b/assets/common/ros2_control.xacro
new file mode 100644
index 00000000..0a6b8ab6
--- /dev/null
+++ b/assets/common/ros2_control.xacro
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+ ${values[keys[index]]}
+
+
+
+
+
+
+
+
+
+ 0.0
+ 0.0
+ 0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${arm_type}
+
+
+ mock_components/GenericSystem
+ ${fake_sensor_commands}
+ 0.0
+
+
+ openarm_hardware/OpenArmHW
+ ${can_interface}
+ ${hardware_prefix}
+ ${can_fd}
+ ${ee_type}
+ ${ee_type != ''}
+ ${control_gains['joint1']['kp']}
+ ${control_gains['joint2']['kp']}
+ ${control_gains['joint3']['kp']}
+ ${control_gains['joint4']['kp']}
+ ${control_gains['joint5']['kp']}
+ ${control_gains['joint6']['kp']}
+ ${control_gains['joint7']['kp']}
+ ${control_gains['joint1']['kd']}
+ ${control_gains['joint2']['kd']}
+ ${control_gains['joint3']['kd']}
+ ${control_gains['joint4']['kd']}
+ ${control_gains['joint5']['kd']}
+ ${control_gains['joint6']['kd']}
+ ${control_gains['joint7']['kd']}
+
+ ${control_gains['hand']['kp']}
+ ${control_gains['hand']['kd']}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml b/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
index c7ee6438..bcc667a9 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
@@ -48,6 +48,8 @@ primary_arm:
enabled: true
product: openarm_v2.0
config: arm
+ control:
+ side: left
inertials: nominal
prefix: openarm_left_
connection:
@@ -70,6 +72,8 @@ secondary_arm:
enabled: true
product: openarm_v2.0
config: arm
+ control:
+ side: right
inertials: nominal
prefix: openarm_right_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml b/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml
index 74f6fcee..dc5d1b07 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml
@@ -44,6 +44,8 @@ primary_arm:
enabled: true
product: openarm_v2.0
config: arm
+ control:
+ side: left
inertials: nominal
prefix: openarm_left_
connection:
@@ -70,6 +72,8 @@ secondary_arm:
enabled: false
product: openarm_v2.0
config: arm
+ control:
+ side: right
inertials: nominal
prefix: openarm_right_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml b/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml
index e1d7a808..65ba7779 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml
@@ -45,6 +45,8 @@ primary_arm:
enabled: true
product: openarm_v2.0
config: arm
+ control:
+ side: left
inertials: nominal
prefix: openarm_left_
connection:
@@ -71,6 +73,8 @@ secondary_arm:
enabled: false
product: openarm_v2.0
config: arm
+ control:
+ side: right
inertials: nominal
prefix: openarm_right_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml b/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml
index d4c68293..19e39241 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml
@@ -44,6 +44,8 @@ primary_arm:
enabled: true
product: openarm_v2.0
config: arm
+ control:
+ side: right
inertials: nominal
prefix: openarm_right_
connection:
@@ -66,6 +68,8 @@ secondary_arm:
enabled: false
product: openarm_v2.0
config: arm
+ control:
+ side: left
inertials: nominal
prefix: openarm_left_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml b/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml
index 248834d0..2fe136ca 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml
@@ -45,6 +45,8 @@ primary_arm:
enabled: true
product: openarm_v2.0
config: arm
+ control:
+ side: right
inertials: nominal
prefix: openarm_right_
connection:
@@ -67,6 +69,8 @@ secondary_arm:
enabled: false
product: openarm_v2.0
config: arm
+ control:
+ side: left
inertials: nominal
prefix: openarm_left_
connection:
diff --git a/assets/robot/openarm_v2.0/urdf/example/v2.urdf b/assets/robot/openarm_v2.0/urdf/example/v2.urdf
index 371f854c..b05adfb3 100644
--- a/assets/robot/openarm_v2.0/urdf/example/v2.urdf
+++ b/assets/robot/openarm_v2.0/urdf/example/v2.urdf
@@ -711,14 +711,6 @@ limitations under the License.
0.0
-
@@ -839,13 +831,5 @@ limitations under the License.
0.0
-
diff --git a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
index 351b2678..0307c7be 100644
--- a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
+++ b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
@@ -16,11 +16,13 @@ limitations under the License.
-
+
+
+
@@ -35,23 +37,10 @@ limitations under the License.
robot_preset="${robot_preset_cfg}"
emit_grasp_frame="$(arg emit_grasp_frame)" />
-
-
-
-
-
-
-
+
+
+
diff --git a/assets/robot/openarm_v2.0/urdf/ros2_control/openarm.bimanual.ros2_control.xacro b/assets/robot/openarm_v2.0/urdf/ros2_control/openarm.bimanual.ros2_control.xacro
deleted file mode 100644
index e7b25ad8..00000000
--- a/assets/robot/openarm_v2.0/urdf/ros2_control/openarm.bimanual.ros2_control.xacro
+++ /dev/null
@@ -1,157 +0,0 @@
-
-
-
-
-
-
-
-
-
- ${arm_type}
-
- mock_components/GenericSystem
- ${fake_sensor_commands}
- 0.0
-
-
- openarm_hardware/OpenArmHW
- ${left_can_interface}
- ${left_arm_prefix}
- ${can_fd}
- ${ee_type}
- ${control_gains['joint1']['kp']}
- ${control_gains['joint2']['kp']}
- ${control_gains['joint3']['kp']}
- ${control_gains['joint4']['kp']}
- ${control_gains['joint5']['kp']}
- ${control_gains['joint6']['kp']}
- ${control_gains['joint7']['kp']}
- ${control_gains['joint1']['kd']}
- ${control_gains['joint2']['kd']}
- ${control_gains['joint3']['kd']}
- ${control_gains['joint4']['kd']}
- ${control_gains['joint5']['kd']}
- ${control_gains['joint6']['kd']}
- ${control_gains['joint7']['kd']}
-
-
-
-
-
-
-
-
-
-
- ${initial_position}
-
-
-
- 0.0
-
-
-
- 0.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${arm_type}
-
- mock_components/GenericSystem
- ${fake_sensor_commands}
- 0.0
-
-
- openarm_hardware/OpenArmHW
- ${right_can_interface}
- ${right_arm_prefix}
- ${can_fd}
- ${ee_type}
- ${control_gains['joint1']['kp']}
- ${control_gains['joint2']['kp']}
- ${control_gains['joint3']['kp']}
- ${control_gains['joint4']['kp']}
- ${control_gains['joint5']['kp']}
- ${control_gains['joint6']['kp']}
- ${control_gains['joint7']['kp']}
- ${control_gains['joint1']['kd']}
- ${control_gains['joint2']['kd']}
- ${control_gains['joint3']['kd']}
- ${control_gains['joint4']['kd']}
- ${control_gains['joint5']['kd']}
- ${control_gains['joint6']['kd']}
- ${control_gains['joint7']['kd']}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/assets/robot/openarm_v2.0/urdf/ros2_control/openarm.ros2_control.xacro b/assets/robot/openarm_v2.0/urdf/ros2_control/openarm.ros2_control.xacro
deleted file mode 100644
index 59f182b2..00000000
--- a/assets/robot/openarm_v2.0/urdf/ros2_control/openarm.ros2_control.xacro
+++ /dev/null
@@ -1,107 +0,0 @@
-
-
-
-
-
-
-
-
-
- ${arm_type}
- ${arm_prefix}
-
- mock_components/GenericSystem
- ${fake_sensor_commands}
- 0.0
-
-
- openarm_hardware/OpenArmHW
- ${can_interface}
- ${arm_prefix}
- ${hand}
- ${can_fd}
- ${ee_type}
- ${control_gains['joint1']['kp']}
- ${control_gains['joint2']['kp']}
- ${control_gains['joint3']['kp']}
- ${control_gains['joint4']['kp']}
- ${control_gains['joint5']['kp']}
- ${control_gains['joint6']['kp']}
- ${control_gains['joint7']['kp']}
- ${control_gains['joint1']['kd']}
- ${control_gains['joint2']['kd']}
- ${control_gains['joint3']['kd']}
- ${control_gains['joint4']['kd']}
- ${control_gains['joint5']['kd']}
- ${control_gains['joint6']['kd']}
- ${control_gains['joint7']['kd']}
-
-
-
-
-
-
-
-
-
-
-
-
- ${initial_position}
-
-
-
- 0.0
-
-
-
- 0.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/launch/display_openarm.launch.py b/launch/display_openarm.launch.py
index 3174b273..4471cff7 100644
--- a/launch/display_openarm.launch.py
+++ b/launch/display_openarm.launch.py
@@ -83,6 +83,9 @@ def robot_state_publisher_spawner(
"arm_type": arm_type_str,
"robot_preset": context.perform_substitution(robot_preset),
"emit_grasp_frame": context.perform_substitution(emit_grasp_frame),
+ "ros2_control": context.perform_substitution(ros2_control),
+ "use_fake_hardware": context.perform_substitution(use_fake_hardware),
+ "fake_sensor_commands": context.perform_substitution(fake_sensor_commands),
}
robot_description = xacro.process_file(
@@ -110,7 +113,7 @@ def generate_launch_description():
robot_preset_arg = DeclareLaunchArgument(
"robot_preset",
default_value="default_bimanual",
- description="Robot preset for v2.0 (e.g. default_bimanual, right_arm, left_arm, right_arm_with_pinch_gripper, left_arm_with_pinch_gripper).",
+ description="Robot preset for v2.0 (e.g. default_bimanual, right_arm, left_arm).",
)
rviz_config_arg = DeclareLaunchArgument(
"rviz_config",
diff --git a/test/support.py b/test/support.py
index 9fe19653..17beda15 100644
--- a/test/support.py
+++ b/test/support.py
@@ -106,17 +106,6 @@ def package_share_at(path):
def expand(files=None, version='v2.0', entry=None, **arguments):
- if entry is None:
- files = dict(files or {})
- files['fixture.urdf.xacro'] = '''
-
-
-
-
-
-'''
- entry = 'assets/fixture.urdf.xacro'
entry = entry or f'assets/robot/openarm_{version}/urdf/openarm_{"v10" if version == "v1.0" else "v20"}.urdf.xacro'
arguments = {k: str(v).lower() if isinstance(v, bool) else str(v)
for k, v in arguments.items()}
diff --git a/test/test_interfaces.py b/test/test_interfaces.py
index 64d7ff1a..9a6c397a 100644
--- a/test/test_interfaces.py
+++ b/test/test_interfaces.py
@@ -67,3 +67,59 @@ def test_broken_component_hook_does_not_use_fallback(interface):
files['body/fixture_a/urdf/component.xacro'] = interface
with pytest.raises((xacro.XacroException, ExpatError)):
support.expand(files, robot_preset='fixture', ros2_control=False)
+
+
+def control_input():
+ names = ['root'] + [f'link{i}' for i in range(1, 8)]
+ data = support.model(names, {f'joint{i}': dict(
+ type='revolute', parent=names[i-1], child=names[i]) for i in range(1, 8)})
+ data['control/control_gains.yaml'] = {
+ f'joint{i}': dict(kp=10+i, kd=i/10) for i in range(1, 8)}
+ spec = support.preset(arm=support.component('robot', 'fixture_control', 'openarm_right_', control=dict(side='right')),
+ tool=support.component('end_effector', 'pinch_gripper', 'openarm_right_', connection=dict(attach_to='arm')))
+ return spec, {'arm': data, 'tool': support.pinch()}
+
+
+def test_control_selection_follows_enabled_components():
+ spec, data = control_input()
+ assert not support.assemble(
+ spec, data, ros2_control=False).findall('ros2_control')
+ for tool_enabled in (False, True):
+ spec['tool']['enabled'] = tool_enabled
+ robot = support.assemble(
+ spec, data, ros2_control=True, use_fake_hardware=True)
+ active = {j.get('name') for j in robot.findall('joint') if j.get(
+ 'type') != 'fixed' and j.find('mimic') is None}
+ assert {j.get('name')
+ for j in robot.findall('ros2_control/joint')} == active
+
+
+def test_control_names_follow_instance_prefixes():
+ spec, data = control_input()
+ spec['arm']['prefix'], spec['tool']['prefix'] = 'drive_', 'tool_'
+ robot = support.assemble(
+ spec, data, ros2_control=True, use_fake_hardware=True)
+ expected = {spec['arm']['prefix'] + j for j in data['arm']
+ ['struct/topology.yaml']['joints']}
+ expected.update(spec['tool']['prefix'] + j for j in data['tool']['struct/topology.yaml']['joints']
+ if j not in data['tool']['joint/joint_mimics.yaml'])
+ assert {j.get('name')
+ for j in robot.findall('ros2_control/joint')} == expected
+
+
+def test_hardware_parameters_follow_input():
+ spec, data = control_input()
+ for side in ('left', 'right'):
+ spec['arm']['control']['side'] = side
+ for name in ('arm', 'tool'):
+ spec[name]['prefix'] = 'openarm_'+side+'_'
+ robot = support.assemble(spec, data, ros2_control=True, use_fake_hardware=False,
+ left_can_interface='can_left_test', right_can_interface='can_right_test', can_fd=False)
+ system = robot.find('ros2_control/hardware')
+ params = {p.get('name'): p.text for p in system.findall('param')}
+ assert params['can_interface'] == 'can_'+side+'_test'
+ assert params['can_fd'].lower() == 'false'
+ assert params['ee_type'] == spec['tool']['product']
+ for name, gains in data['arm']['control/control_gains.yaml'].items():
+ for key, value in gains.items():
+ assert float(params[key+name.removeprefix('joint')]) == value
diff --git a/test/test_models.py b/test/test_models.py
new file mode 100644
index 00000000..57716a88
--- /dev/null
+++ b/test/test_models.py
@@ -0,0 +1,43 @@
+# Copyright 2026 Enactic, Inc.
+#
+# 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.
+
+from pathlib import Path
+from urllib.parse import urlparse
+
+from support import ROOT, expand, packages
+
+
+def test_shipped_presets_expand_and_resolve_resources():
+ robots = []
+ for version in ('v2.0',):
+ folder = ROOT/f'assets/robot/openarm_{version}/config/robot_presets'
+ robots.append((version, '', expand(version=version, ros2_control=True,
+ use_fake_hardware=True, emit_grasp_frame=True)))
+ for path in sorted(folder.glob('*.yaml')):
+ robot = expand(version=version, robot_preset=path.stem, ros2_control=True,
+ use_fake_hardware=True, emit_grasp_frame=True)
+ robots.append((version, path.stem, robot))
+ for path in sorted((ROOT/'assets/end_effector').glob('pinch_gripper/urdf/*_standalone.urdf.xacro')):
+ robots.append(('standalone', path.name, expand(
+ entry=str(path.relative_to(ROOT)))))
+ for version, name, robot in robots:
+ for mesh in robot.findall('.//geometry/mesh'):
+ uri = urlparse(mesh.get('filename'))
+ if uri.scheme == 'package':
+ share = ROOT if uri.netloc == 'openarm_description' else Path(
+ packages.get_package_share_directory(uri.netloc))
+ assert (share/uri.path.lstrip('/')
+ ).is_file(), (version, name, mesh.attrib)
+ elif uri.scheme in ('', 'file'):
+ assert Path(uri.path).is_file(), (version, name, mesh.attrib)
From 291c0d0c5ebccebb7563b58228e57894767e00d6 Mon Sep 17 00:00:00 2001
From: li-strix <128799116+stevenaya@users.noreply.github.com>
Date: Wed, 4 Nov 2026 18:12:54 +0900
Subject: [PATCH 2/4] Check control selection and resolved names in one fixture
---
test/test_interfaces.py | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/test/test_interfaces.py b/test/test_interfaces.py
index 9a6c397a..3a8617cd 100644
--- a/test/test_interfaces.py
+++ b/test/test_interfaces.py
@@ -80,31 +80,31 @@ def control_input():
return spec, {'arm': data, 'tool': support.pinch()}
-def test_control_selection_follows_enabled_components():
+def test_control_joints_follow_model_names_and_enabled_components():
spec, data = control_input()
+ spec['arm']['prefix'], spec['tool']['prefix'] = 'drive_', 'tool_'
+ arm_joints = data['arm']['struct/topology.yaml']['joints']
+ arm_joints['wrist'] = arm_joints.pop('joint7')
+ for filename in ('joint_origins', 'joint_axes', 'joint_limits'):
+ values = data['arm'][f'joint/{filename}.yaml']
+ values['wrist'] = values.pop('joint7')
+
assert not support.assemble(
spec, data, ros2_control=False).findall('ros2_control')
for tool_enabled in (False, True):
spec['tool']['enabled'] = tool_enabled
robot = support.assemble(
spec, data, ros2_control=True, use_fake_hardware=True)
- active = {j.get('name') for j in robot.findall('joint') if j.get(
- 'type') != 'fixed' and j.find('mimic') is None}
- assert {j.get('name')
- for j in robot.findall('ros2_control/joint')} == active
-
-
-def test_control_names_follow_instance_prefixes():
- spec, data = control_input()
- spec['arm']['prefix'], spec['tool']['prefix'] = 'drive_', 'tool_'
- robot = support.assemble(
- spec, data, ros2_control=True, use_fake_hardware=True)
- expected = {spec['arm']['prefix'] + j for j in data['arm']
- ['struct/topology.yaml']['joints']}
- expected.update(spec['tool']['prefix'] + j for j in data['tool']['struct/topology.yaml']['joints']
- if j not in data['tool']['joint/joint_mimics.yaml'])
- assert {j.get('name')
- for j in robot.findall('ros2_control/joint')} == expected
+ expected = {spec['arm']['prefix'] + name for name in arm_joints}
+ if tool_enabled:
+ expected.update(spec['tool']['prefix'] + name
+ for name in data['tool']['struct/topology.yaml']['joints']
+ if name not in data['tool']['joint/joint_mimics.yaml'])
+ active = {joint.get('name') for joint in robot.findall('joint')
+ if joint.get('type') != 'fixed' and joint.find('mimic') is None}
+ assert active == expected
+ assert {joint.get('name')
+ for joint in robot.findall('ros2_control/joint')} == active
def test_hardware_parameters_follow_input():
From fc349ed75c7a5bc169f84bb4a75a2196caad12c2 Mon Sep 17 00:00:00 2001
From: li-strix <128799116+stevenaya@users.noreply.github.com>
Date: Wed, 4 Nov 2026 20:14:09 +0900
Subject: [PATCH 3/4] Share resolved component descriptions with control
generation
---
assets/common/assembly.xacro | 59 ++++++++-----------
assets/common/ros2_control.xacro | 19 +++---
assets/component_loader.xacro | 3 +-
.../openarm_v2.0/urdf/openarm_v20.urdf.xacro | 4 +-
test/test_interfaces.py | 20 +++++++
5 files changed, 56 insertions(+), 49 deletions(-)
diff --git a/assets/common/assembly.xacro b/assets/common/assembly.xacro
index bdf77768..812aefea 100644
--- a/assets/common/assembly.xacro
+++ b/assets/common/assembly.xacro
@@ -16,23 +16,21 @@ limitations under the License.
-
+
-
-
-
-
+
+
-
+
+ params="component_descriptions component_name components robot_preset config_root root_frame component_cfg component_desc parent_desc:=none emit_grasp_frame:=false out_name">
@@ -134,8 +132,9 @@ limitations under the License.
value="${[name for name in attached_children if component_terminal_link_empty and 'parent_reference_point' in robot_preset[name]['connection'] and robot_preset[name]['connection']['parent_reference_point'] != None and ('parent' not in robot_preset[name]['connection'] or robot_preset[name]['connection']['parent'] == None) and robot_preset[name]['connection']['parent_reference_point'] in component_desc['reference_points'] and component_desc['reference_points'][robot_preset[name]['connection']['parent_reference_point']]['parent'] == component_terminal_link]}"
lazy_eval="false" />
-
+
@@ -170,20 +169,12 @@ limitations under the License.
+ params="component_api component_descriptions component_name components robot_preset config_root root_frame parent_desc:=none emit_grasp_frame:=false">
-
-
+
-
-
-
-
-
-
+ params="component_api component_descriptions index components parent_component_name robot_preset config_root root_frame parent_desc:=none emit_grasp_frame:=false">
@@ -229,22 +218,19 @@ limitations under the License.
-
-
-
-
+ params="component_api config_root robot_preset emit_grasp_frame:=false out_name:=component_descriptions">
@@ -269,9 +253,14 @@ limitations under the License.
components="${components}"
robot_preset="${robot_preset}" />
+
+
+
+
-
-
+ params="robot_preset component_descriptions config_root can_interfaces arm_type:=v20 use_fake_hardware:=true fake_sensor_commands:=false can_fd:=true index:=0">
+
@@ -92,9 +92,9 @@ limitations under the License.
-
-
-
+
+
+
@@ -103,10 +103,9 @@ limitations under the License.
-
-
-
-
+
+
+
-
diff --git a/assets/component_loader.xacro b/assets/component_loader.xacro
index a73afbdc..f4536023 100644
--- a/assets/component_loader.xacro
+++ b/assets/component_loader.xacro
@@ -47,13 +47,12 @@ limitations under the License.
-
+ component_cfg="${component_cfg}" component_desc="${component_desc}" component_descriptions="${dict(component=component_desc)}" out_name="component_runtime" />
diff --git a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
index 0307c7be..d4cd762f 100644
--- a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
+++ b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
@@ -32,13 +32,13 @@ limitations under the License.
-
-
diff --git a/test/test_interfaces.py b/test/test_interfaces.py
index 3a8617cd..4ce4c8f4 100644
--- a/test/test_interfaces.py
+++ b/test/test_interfaces.py
@@ -106,6 +106,26 @@ def test_control_joints_follow_model_names_and_enabled_components():
assert {joint.get('name')
for joint in robot.findall('ros2_control/joint')} == active
+ data['arm']['struct/topology.yaml']['collapse_empty_end'] = True
+ spec['tool']['connection']['parent_reference_point'] = 'port'
+ files = support.files_for(spec, data)
+ files['end_effector/pinch_gripper/urdf/component.xacro'] = '''
+
+
+
+
+ '''
+ robot = support.expand(files, robot_preset='fixture',
+ ros2_control=True, use_fake_hardware=True)
+ assert robot.find("joint[@name='tool_finger_joint2']/mimic") is None
+ expected.add('tool_finger_joint2')
+ active = {joint.get('name') for joint in robot.findall('joint')
+ if joint.get('type') != 'fixed' and joint.find('mimic') is None}
+ assert active == expected
+ assert {joint.get('name')
+ for joint in robot.findall('ros2_control/joint')} == active
+
def test_hardware_parameters_follow_input():
spec, data = control_input()
From 416bdee5d45e6cdb28f5fd8134ceb3fc46e38579 Mon Sep 17 00:00:00 2001
From: li-strix <128799116+stevenaya@users.noreply.github.com>
Date: Wed, 4 Nov 2026 20:14:10 +0900
Subject: [PATCH 4/4] Flatten preset control settings and select mock hardware
automatically
---
README.md | 2 +
assets/common/ros2_control.xacro | 133 +++++++++---------
.../robot_presets/default_bimanual.yaml | 10 +-
.../config/robot_presets/left_arm.yaml | 10 +-
.../left_arm_with_pinch_gripper.yaml | 10 +-
.../config/robot_presets/right_arm.yaml | 10 +-
.../right_arm_with_pinch_gripper.yaml | 10 +-
.../openarm_v2.0/urdf/openarm_v20.urdf.xacro | 8 +-
test/test_interfaces.py | 32 +++--
9 files changed, 132 insertions(+), 93 deletions(-)
diff --git a/README.md b/README.md
index 15c33176..bd1bdc1e 100644
--- a/README.md
+++ b/README.md
@@ -49,3 +49,5 @@ Copyright 2025 Enactic, Inc.
All participation in the OpenArm project is governed by our
[Code of Conduct](CODE_OF_CONDUCT.md).
+
+Control settings live directly under each component's `control` mapping. Set `hardware_plugin`, `can_interface` and `can_fd` in the preset; `use_fake_hardware:=true` selects `mock_components/GenericSystem`. The optional `system_name` defaults to `_hardware_interface`. Explicit control parameters override generated defaults and are emitted once. Components declare their generated topology, names and mimic relationships in `describe_component`; assembly and control consume that shared description.
diff --git a/assets/common/ros2_control.xacro b/assets/common/ros2_control.xacro
index 0c49c96a..ab3b3b87 100644
--- a/assets/common/ros2_control.xacro
+++ b/assets/common/ros2_control.xacro
@@ -14,6 +14,7 @@ limitations under the License.
+
@@ -40,83 +41,79 @@ limitations under the License.
-
-
-
- ${arm_type}
-
-
- mock_components/GenericSystem
- ${fake_sensor_commands}
- 0.0
-
-
- openarm_hardware/OpenArmHW
- ${can_interface}
- ${hardware_prefix}
- ${can_fd}
- ${ee_type}
- ${ee_type != ''}
- ${control_gains['joint1']['kp']}
- ${control_gains['joint2']['kp']}
- ${control_gains['joint3']['kp']}
- ${control_gains['joint4']['kp']}
- ${control_gains['joint5']['kp']}
- ${control_gains['joint6']['kp']}
- ${control_gains['joint7']['kp']}
- ${control_gains['joint1']['kd']}
- ${control_gains['joint2']['kd']}
- ${control_gains['joint3']['kd']}
- ${control_gains['joint4']['kd']}
- ${control_gains['joint5']['kd']}
- ${control_gains['joint6']['kd']}
- ${control_gains['joint7']['kd']}
-
- ${control_gains['hand']['kp']}
- ${control_gains['hand']['kd']}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ params="robot_preset component_descriptions arm_type:=v20 use_fake_hardware:=true fake_sensor_commands:=false index:=0">
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${plugin}
+
+
+
+
-
+
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml b/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
index bcc667a9..5e90ded9 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
@@ -49,7 +49,10 @@ primary_arm:
product: openarm_v2.0
config: arm
control:
- side: left
+ system_name: openarm_left_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can1
+ can_fd: true
inertials: nominal
prefix: openarm_left_
connection:
@@ -73,7 +76,10 @@ secondary_arm:
product: openarm_v2.0
config: arm
control:
- side: right
+ system_name: openarm_right_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can0
+ can_fd: true
inertials: nominal
prefix: openarm_right_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml b/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml
index dc5d1b07..6c4c4de9 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/left_arm.yaml
@@ -45,7 +45,10 @@ primary_arm:
product: openarm_v2.0
config: arm
control:
- side: left
+ system_name: openarm_left_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can1
+ can_fd: true
inertials: nominal
prefix: openarm_left_
connection:
@@ -73,7 +76,10 @@ secondary_arm:
product: openarm_v2.0
config: arm
control:
- side: right
+ system_name: openarm_right_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can0
+ can_fd: true
inertials: nominal
prefix: openarm_right_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml b/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml
index 65ba7779..69e592e3 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/left_arm_with_pinch_gripper.yaml
@@ -46,7 +46,10 @@ primary_arm:
product: openarm_v2.0
config: arm
control:
- side: left
+ system_name: openarm_left_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can1
+ can_fd: true
inertials: nominal
prefix: openarm_left_
connection:
@@ -74,7 +77,10 @@ secondary_arm:
product: openarm_v2.0
config: arm
control:
- side: right
+ system_name: openarm_right_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can0
+ can_fd: true
inertials: nominal
prefix: openarm_right_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml b/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml
index 19e39241..12c981b1 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/right_arm.yaml
@@ -45,7 +45,10 @@ primary_arm:
product: openarm_v2.0
config: arm
control:
- side: right
+ system_name: openarm_right_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can0
+ can_fd: true
inertials: nominal
prefix: openarm_right_
connection:
@@ -69,7 +72,10 @@ secondary_arm:
product: openarm_v2.0
config: arm
control:
- side: left
+ system_name: openarm_left_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can1
+ can_fd: true
inertials: nominal
prefix: openarm_left_
connection:
diff --git a/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml b/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml
index 2fe136ca..04ada1c8 100644
--- a/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml
+++ b/assets/robot/openarm_v2.0/config/robot_presets/right_arm_with_pinch_gripper.yaml
@@ -46,7 +46,10 @@ primary_arm:
product: openarm_v2.0
config: arm
control:
- side: right
+ system_name: openarm_right_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can0
+ can_fd: true
inertials: nominal
prefix: openarm_right_
connection:
@@ -70,7 +73,10 @@ secondary_arm:
product: openarm_v2.0
config: arm
control:
- side: left
+ system_name: openarm_left_hardware_interface
+ hardware_plugin: openarm_hardware/OpenArmHW
+ can_interface: can1
+ can_fd: true
inertials: nominal
prefix: openarm_left_
connection:
diff --git a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
index d4cd762f..8c1de8db 100644
--- a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
+++ b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
@@ -24,9 +24,6 @@ limitations under the License.
-
-
-
@@ -38,9 +35,8 @@ limitations under the License.
emit_grasp_frame="$(arg emit_grasp_frame)" />
-
+
diff --git a/test/test_interfaces.py b/test/test_interfaces.py
index 4ce4c8f4..b07bd0cf 100644
--- a/test/test_interfaces.py
+++ b/test/test_interfaces.py
@@ -75,7 +75,7 @@ def control_input():
type='revolute', parent=names[i-1], child=names[i]) for i in range(1, 8)})
data['control/control_gains.yaml'] = {
f'joint{i}': dict(kp=10+i, kd=i/10) for i in range(1, 8)}
- spec = support.preset(arm=support.component('robot', 'fixture_control', 'openarm_right_', control=dict(side='right')),
+ spec = support.preset(arm=support.component('robot', 'fixture_control', 'openarm_right_', control=dict(hardware_plugin='openarm_hardware/OpenArmHW', can_interface='can_test', can_fd=True)),
tool=support.component('end_effector', 'pinch_gripper', 'openarm_right_', connection=dict(attach_to='arm')))
return spec, {'arm': data, 'tool': support.pinch()}
@@ -129,17 +129,31 @@ def test_control_joints_follow_model_names_and_enabled_components():
def test_hardware_parameters_follow_input():
spec, data = control_input()
- for side in ('left', 'right'):
- spec['arm']['control']['side'] = side
+ for prefix, can_interface in [('openarm_left_', 'can_left_test'),
+ ('openarm_right_', 'can_right_test')]:
+ control = spec['arm']['control']
+ control.update(can_interface=can_interface, can_fd=False)
for name in ('arm', 'tool'):
- spec[name]['prefix'] = 'openarm_'+side+'_'
- robot = support.assemble(spec, data, ros2_control=True, use_fake_hardware=False,
- left_can_interface='can_left_test', right_can_interface='can_right_test', can_fd=False)
- system = robot.find('ros2_control/hardware')
- params = {p.get('name'): p.text for p in system.findall('param')}
- assert params['can_interface'] == 'can_'+side+'_test'
+ spec[name]['prefix'] = prefix
+ robot = support.assemble(spec, data, ros2_control=True,
+ use_fake_hardware=False)
+ hardware = robot.find('ros2_control/hardware')
+ assert hardware.find('plugin').text == control['hardware_plugin']
+ params = {p.get('name'): p.text for p in hardware.findall('param')}
+ assert len(params) == len(hardware.findall('param'))
+ assert params['can_interface'] == control['can_interface']
assert params['can_fd'].lower() == 'false'
+ assert params['arm_prefix'] == prefix.removeprefix('openarm_')
assert params['ee_type'] == spec['tool']['product']
for name, gains in data['arm']['control/control_gains.yaml'].items():
for key, value in gains.items():
assert float(params[key+name.removeprefix('joint')]) == value
+
+ robot = support.assemble(spec, data, ros2_control=True,
+ use_fake_hardware=True, fake_sensor_commands=True)
+ hardware = robot.find('ros2_control/hardware')
+ assert hardware.find('plugin').text == 'mock_components/GenericSystem'
+ params = {p.get('name'): p.text for p in hardware.findall('param')}
+ assert params['mock_sensor_commands'].lower() == 'true'
+ assert float(params['position_state_following_offset']) == 0.0
+ assert 'fake_sensor_commands' not in params and 'state_following_offset' not in params