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