From 2996035d5500656aa0757d3f6ebb5a2f36d958a1 Mon Sep 17 00:00:00 2001
From: li-strix <128799116+stevenaya@users.noreply.github.com>
Date: Fri, 6 Nov 2026 19:24:40 +0900
Subject: [PATCH 1/2] Separate stable component keys from emitted link and
joint names
---
README.md | 2 +
.../body/v2.0/config/inertials/nominal.yaml | 2 +-
assets/body/v2.0/config/link/collisions.yaml | 2 +-
assets/body/v2.0/config/link/visuals.yaml | 2 +-
.../v2.0/config/struct/reference_points.yaml | 10 ++--
assets/body/v2.0/config/struct/topology.yaml | 3 +-
assets/common/assembly.xacro | 8 +--
assets/common/components.xacro | 14 ++++--
assets/common/kinematic_chain.xacro | 24 +++++----
assets/common/links.xacro | 2 +-
assets/common/validation.xacro | 12 +++--
.../config/inertials/nominal.yaml | 6 +--
.../config/joint/joint_axes.yaml | 4 +-
.../config/joint/joint_limits.yaml | 4 +-
.../config/joint/joint_mimics.yaml | 4 +-
.../config/joint/joint_origins.yaml | 4 +-
.../pinch_gripper/config/link/collisions.yaml | 6 +--
.../pinch_gripper/config/link/visuals.yaml | 6 +--
.../config/struct/name_mapping.yaml | 20 ++++----
.../config/struct/reference_points.yaml | 6 +--
.../pinch_gripper/config/struct/topology.yaml | 23 +++++----
.../pinch_gripper/urdf/component.xacro | 2 +
.../urdf/pinch_gripper_macro.xacro | 35 ++++++-------
.../config/arm/inertials/nominal.yaml | 14 +++---
.../config/arm/link/collisions.yaml | 14 +++---
.../openarm_v2.0/config/arm/link/visuals.yaml | 14 +++---
.../config/arm/struct/name_mapping.yaml | 32 ++++++------
.../config/arm/struct/reference_points.yaml | 4 +-
.../config/arm/struct/topology.yaml | 43 +++++++++-------
.../robot_presets/default_bimanual.yaml | 2 +-
.../config/robot_presets/left_arm.yaml | 2 +-
.../left_arm_with_pinch_gripper.yaml | 2 +-
.../openarm_v2.0/urdf/openarm_v20.urdf.xacro | 2 +-
assets/sensor/zed/urdf/component.xacro | 5 +-
test/support.py | 17 ++++---
test/test_assembly.py | 50 ++++++++++++-------
test/test_entrypoints.py | 8 +--
37 files changed, 230 insertions(+), 180 deletions(-)
diff --git a/README.md b/README.md
index 15c33176..7f82e7cf 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ The default implementations are `openarm_describe_config_component` and `openarm
Interface files declare macros; implementation and third-party model includes belong inside those macros. Each interface is loaded in a local scope, so its definitions do not affect other components. Presets need no implementation selector or product registry.
+Component topology uses stable `link1`, `link2`, ... and `joint1`, `joint2`, ... keys. An optional `name` on each topology entry defines its local URDF name; otherwise the key is used. The instance prefix is added once. Geometry, inertia, joint parameters, parent/child links, mimic targets, references, reflection selections and overrides use the stable keys. Changing `name` preserves these config references. Component descriptors expose resolved `link_name_map` and `joint_name_map` entries for emitters and control generation.
+
## Related links
- 📚 Read the [documentation](https://docs.openarm.dev/software/description)
diff --git a/assets/body/v2.0/config/inertials/nominal.yaml b/assets/body/v2.0/config/inertials/nominal.yaml
index 75949159..52fdec86 100644
--- a/assets/body/v2.0/config/inertials/nominal.yaml
+++ b/assets/body/v2.0/config/inertials/nominal.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-body_link0:
+link1:
origin:
x: -0.01627169562
y: 0.0
diff --git a/assets/body/v2.0/config/link/collisions.yaml b/assets/body/v2.0/config/link/collisions.yaml
index 46fb7013..7e1d776d 100644
--- a/assets/body/v2.0/config/link/collisions.yaml
+++ b/assets/body/v2.0/config/link/collisions.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-body_link0:
+link1:
origin:
x: 0.0
y: 0.0
diff --git a/assets/body/v2.0/config/link/visuals.yaml b/assets/body/v2.0/config/link/visuals.yaml
index f1fa731c..474ec659 100644
--- a/assets/body/v2.0/config/link/visuals.yaml
+++ b/assets/body/v2.0/config/link/visuals.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-body_link0:
+link1:
origin:
x: 0.0
y: 0.0
diff --git a/assets/body/v2.0/config/struct/reference_points.yaml b/assets/body/v2.0/config/struct/reference_points.yaml
index bf695c6c..38e13a41 100644
--- a/assets/body/v2.0/config/struct/reference_points.yaml
+++ b/assets/body/v2.0/config/struct/reference_points.yaml
@@ -13,7 +13,7 @@
# limitations under the License.
left_arm_mount_point:
- parent: body_link0
+ parent: link1
origin:
x: 0.0
y: 0.030
@@ -29,13 +29,13 @@ left_arm_mount_point:
type: fixed
parent_link:
original_name: body_link0
- canonical_name: body_link0
+ canonical_name: link1
child_link:
original_name: body_left_arm_mount_point
canonical_name: null
right_arm_mount_point:
- parent: body_link0
+ parent: link1
origin:
x: 0.0
y: -0.030
@@ -51,13 +51,13 @@ right_arm_mount_point:
type: fixed
parent_link:
original_name: body_link0
- canonical_name: body_link0
+ canonical_name: link1
child_link:
original_name: body_right_arm_mount_point
canonical_name: null
arm_origin:
- parent: body_link0
+ parent: link1
emit: true
origin:
x: 0.0
diff --git a/assets/body/v2.0/config/struct/topology.yaml b/assets/body/v2.0/config/struct/topology.yaml
index 59402442..97b46d61 100644
--- a/assets/body/v2.0/config/struct/topology.yaml
+++ b/assets/body/v2.0/config/struct/topology.yaml
@@ -13,5 +13,6 @@
# limitations under the License.
links:
- - body_link0
+ link1:
+ name: body_link0
joints: {}
diff --git a/assets/common/assembly.xacro b/assets/common/assembly.xacro
index c67a42ac..e24c8c6a 100644
--- a/assets/common/assembly.xacro
+++ b/assets/common/assembly.xacro
@@ -96,7 +96,7 @@ limitations under the License.
-
+
@@ -111,9 +111,9 @@ limitations under the License.
-
+
@@ -123,7 +123,7 @@ limitations under the License.
name="attached_children"
value="${[name for name in components if name in robot_preset and robot_preset[name]['enabled'] and 'connection' in robot_preset[name] and 'attach_to' in robot_preset[name]['connection'] and robot_preset[name]['connection']['attach_to'] == component_name]}"
lazy_eval="false" />
-
+
-
-
-
-
+
+
+
+
+
+ params="index joint_names topology prefix link_name_map joint_name_map joint_origins joint_axes joint_limits joint_mimics inertials visuals collisions reflect_x reflect_y reflect_z reflect_include_links reflect_include_joints origin_override_joints limit_override_joints tip_collapse_source_link tip_collapse_child_link base_frame_shift">
@@ -26,20 +26,21 @@ limitations under the License.
-
-
+ frame_shift="${base_frame_shift if joint['parent'] == list(topology['links'])[0] else 'none'}" />
+ params="connected_to mount_origin topology prefix link_name_map joint_name_map joint_origins joint_axes joint_limits joint_mimics:=${dict()} inertials visuals collisions reflect_x:=1 reflect_y:=1 reflect_z:=1 reflect_include_links:=${[]} reflect_include_joints:=${[]} origin_override_joints:=${dict()} limit_override_joints:=${dict()} tip_collapse_source_link:=none tip_collapse_child_link:=none skip_mount_joint:=${False} base_frame_shift:=none">
-
+
-
-
+
-
+
-
+
diff --git a/assets/common/validation.xacro b/assets/common/validation.xacro
index e772915c..5bcee7d5 100644
--- a/assets/common/validation.xacro
+++ b/assets/common/validation.xacro
@@ -137,7 +137,7 @@ limitations under the License.
-
+
@@ -146,10 +146,16 @@ limitations under the License.
-
+
-
+
+
+
+
+
diff --git a/assets/end_effector/pinch_gripper/config/inertials/nominal.yaml b/assets/end_effector/pinch_gripper/config/inertials/nominal.yaml
index 6bfd2f4b..05ebfae2 100644
--- a/assets/end_effector/pinch_gripper/config/inertials/nominal.yaml
+++ b/assets/end_effector/pinch_gripper/config/inertials/nominal.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ee_base_link:
+link1:
origin:
x: -0.0067098
y: 0.008407299999999999
@@ -28,7 +28,7 @@ ee_base_link:
yy: 0.00015627
yz: -1.5124e-06
zz: 0.0001659
-ee_link1:
+link2:
origin:
x: -0.0124990911725494
y: 0.00176592789915655
@@ -44,7 +44,7 @@ ee_link1:
yy: 3.62137424319396e-05
yz: -3.60343788693423e-06
zz: 1.74958961632907e-05
-ee_link2:
+link3:
origin:
x: -0.012796
y: -0.0020183
diff --git a/assets/end_effector/pinch_gripper/config/joint/joint_axes.yaml b/assets/end_effector/pinch_gripper/config/joint/joint_axes.yaml
index 13cc0ae1..c8d738fa 100644
--- a/assets/end_effector/pinch_gripper/config/joint/joint_axes.yaml
+++ b/assets/end_effector/pinch_gripper/config/joint/joint_axes.yaml
@@ -12,12 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-finger_joint1:
+joint1:
axis:
x: -1
y: 0
z: 0
-finger_joint2:
+joint2:
axis:
x: 1
y: 0
diff --git a/assets/end_effector/pinch_gripper/config/joint/joint_limits.yaml b/assets/end_effector/pinch_gripper/config/joint/joint_limits.yaml
index 19942211..8dfc86de 100644
--- a/assets/end_effector/pinch_gripper/config/joint/joint_limits.yaml
+++ b/assets/end_effector/pinch_gripper/config/joint/joint_limits.yaml
@@ -12,13 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-finger_joint1:
+joint1:
limit:
lower: -0.7854
upper: 0
effort: 7
velocity: 5
-finger_joint2:
+joint2:
limit:
lower: -0.7854
upper: 0
diff --git a/assets/end_effector/pinch_gripper/config/joint/joint_mimics.yaml b/assets/end_effector/pinch_gripper/config/joint/joint_mimics.yaml
index 1c0f9656..48434cde 100644
--- a/assets/end_effector/pinch_gripper/config/joint/joint_mimics.yaml
+++ b/assets/end_effector/pinch_gripper/config/joint/joint_mimics.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-finger_joint2:
- joint: finger_joint1
+joint2:
+ joint: joint1
multiplier: 1
offset: 0
diff --git a/assets/end_effector/pinch_gripper/config/joint/joint_origins.yaml b/assets/end_effector/pinch_gripper/config/joint/joint_origins.yaml
index cc5b65bf..da58c50f 100644
--- a/assets/end_effector/pinch_gripper/config/joint/joint_origins.yaml
+++ b/assets/end_effector/pinch_gripper/config/joint/joint_origins.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-finger_joint1:
+joint1:
origin:
x: -0.0219300000000005
y: 0.0180000000000009
@@ -20,7 +20,7 @@ finger_joint1:
roll: 0
pitch: -0
yaw: 0
-finger_joint2:
+joint2:
origin:
x: -0.0219300000000005
y: -0.0180000000000012
diff --git a/assets/end_effector/pinch_gripper/config/link/collisions.yaml b/assets/end_effector/pinch_gripper/config/link/collisions.yaml
index 2dfa0c8e..1f4d4c1b 100644
--- a/assets/end_effector/pinch_gripper/config/link/collisions.yaml
+++ b/assets/end_effector/pinch_gripper/config/link/collisions.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ee_base_link:
+link1:
origin:
x: 0
y: 0
@@ -26,7 +26,7 @@ ee_base_link:
x: 1
y: 1
z: 1
-ee_link1:
+link2:
origin:
x: 0
y: 0
@@ -40,7 +40,7 @@ ee_link1:
x: 1
y: 1
z: 1
-ee_link2:
+link3:
origin:
x: 0
y: 0
diff --git a/assets/end_effector/pinch_gripper/config/link/visuals.yaml b/assets/end_effector/pinch_gripper/config/link/visuals.yaml
index d6669034..144aca0d 100644
--- a/assets/end_effector/pinch_gripper/config/link/visuals.yaml
+++ b/assets/end_effector/pinch_gripper/config/link/visuals.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ee_base_link:
+link1:
origin:
x: 0
y: 0
@@ -26,7 +26,7 @@ ee_base_link:
x: 1
y: 1
z: 1
-ee_link1:
+link2:
origin:
x: 0
y: 0
@@ -40,7 +40,7 @@ ee_link1:
x: 1
y: 1
z: 1
-ee_link2:
+link3:
origin:
x: 0
y: 0
diff --git a/assets/end_effector/pinch_gripper/config/struct/name_mapping.yaml b/assets/end_effector/pinch_gripper/config/struct/name_mapping.yaml
index 84555f5c..b6300337 100644
--- a/assets/end_effector/pinch_gripper/config/struct/name_mapping.yaml
+++ b/assets/end_effector/pinch_gripper/config/struct/name_mapping.yaml
@@ -14,17 +14,17 @@
links:
canonical_to_original:
- ee_base_link: ee_base_link
- ee_link1: finger_inner
- ee_link2: finger_outer
+ link1: ee_base_link
+ link2: finger_inner
+ link3: finger_outer
original_to_canonical:
- ee_base_link: ee_base_link
- finger_inner: ee_link1
- finger_outer: ee_link2
+ ee_base_link: link1
+ finger_inner: link2
+ finger_outer: link3
joints:
canonical_to_original:
- finger_joint1: finger_inner_joint
- finger_joint2: finger_outer_joint
+ joint1: finger_inner_joint
+ joint2: finger_outer_joint
original_to_canonical:
- finger_inner_joint: finger_joint1
- finger_outer_joint: finger_joint2
+ finger_inner_joint: joint1
+ finger_outer_joint: joint2
diff --git a/assets/end_effector/pinch_gripper/config/struct/reference_points.yaml b/assets/end_effector/pinch_gripper/config/struct/reference_points.yaml
index e3cbf358..51344d31 100644
--- a/assets/end_effector/pinch_gripper/config/struct/reference_points.yaml
+++ b/assets/end_effector/pinch_gripper/config/struct/reference_points.yaml
@@ -13,7 +13,7 @@
# limitations under the License.
grasp_frame:
- parent: ee_base_link
+ parent: link1
origin:
x: -0.0219300000000009
y: 0
@@ -29,13 +29,13 @@ grasp_frame:
type: fixed
parent_link:
original_name: ee_base_link
- canonical_name: ee_base_link
+ canonical_name: link1
child_link:
original_name: grasp_frame
canonical_name: null
camera_view_frame:
- parent: ee_base_link
+ parent: link1
emit: true
origin:
x: 0.0013353
diff --git a/assets/end_effector/pinch_gripper/config/struct/topology.yaml b/assets/end_effector/pinch_gripper/config/struct/topology.yaml
index 9b3784e2..ff2863cb 100644
--- a/assets/end_effector/pinch_gripper/config/struct/topology.yaml
+++ b/assets/end_effector/pinch_gripper/config/struct/topology.yaml
@@ -13,15 +13,20 @@
# limitations under the License.
links:
-- ee_base_link
-- ee_link1
-- ee_link2
+ link1:
+ name: ee_base_link
+ link2:
+ name: ee_link1
+ link3:
+ name: ee_link2
joints:
- finger_joint1:
+ joint1:
+ name: finger_joint1
type: revolute
- parent: ee_base_link
- child: ee_link1
- finger_joint2:
+ parent: link1
+ child: link2
+ joint2:
+ name: finger_joint2
type: revolute
- parent: ee_base_link
- child: ee_link2
+ parent: link1
+ child: link3
diff --git a/assets/end_effector/pinch_gripper/urdf/component.xacro b/assets/end_effector/pinch_gripper/urdf/component.xacro
index 4c130677..62f9ea4a 100644
--- a/assets/end_effector/pinch_gripper/urdf/component.xacro
+++ b/assets/end_effector/pinch_gripper/urdf/component.xacro
@@ -28,6 +28,8 @@ limitations under the License.
+ params="prefix connected_to mount_origin:=none topology link_name_map joint_name_map joint_origins joint_axes joint_limits joint_mimics inertials visuals collisions reflect_x:=1 reflect_y:=1 reflect_z:=1 component_reflects:=false reflect_include_links:=^ reflect_include_joints:=^ reflect_include_reference_points:=^ origin_override_joints:=${dict()} limit_override_joints:=^ ee_reference_points:=^ emit_grasp_frame:=false grasp_reference_point:=grasp_frame skip_mount_joint:=false base_frame_shift:=none">
-
+
-
+
-
-
-
-
+
+
+
+
@@ -44,8 +44,8 @@ limitations under the License.
-
-
+
+
-
+
-
-
+
+
@@ -139,15 +139,15 @@ limitations under the License.
-
+
-
-
+
+
@@ -164,6 +164,7 @@ limitations under the License.
reference_points="${ee_reference_points}"
force_emit_reference="${grasp_reference_point if emit_grasp_frame else ''}"
prefix="${prefix}"
+ link_name_map="${link_name_map}"
base_link_name="${base_link_name}"
reflect_x="${reflect_x if component_reflects else 1}"
reflect_y="${reflect_y if component_reflects else 1}"
diff --git a/assets/robot/openarm_v2.0/config/arm/inertials/nominal.yaml b/assets/robot/openarm_v2.0/config/arm/inertials/nominal.yaml
index a6f5f7ec..6d7df71d 100644
--- a/assets/robot/openarm_v2.0/config/arm/inertials/nominal.yaml
+++ b/assets/robot/openarm_v2.0/config/arm/inertials/nominal.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-base_link:
+link1:
origin:
x: 0.00253123748061566
y: -0.0258185540330169
@@ -28,7 +28,7 @@ base_link:
yy: 0.00159935208714031
yz: 2.7521421737084e-08
zz: 0.000750541980863571
-link1:
+link2:
origin:
x: 0.000900350355624244
y: -0.0444659716892874
@@ -44,7 +44,7 @@ link1:
yy: 0.000759199728161993
yz: 6.66755698859637e-08
zz: 0.000670659201019629
-link2:
+link3:
origin:
x: -0.0247184924346635
y: 7.60578399428002e-08
@@ -60,7 +60,7 @@ link2:
yy: 0.000113062180225725
yz: 3.75816911469744e-10
zz: 8.22999351863579e-05
-link3:
+link4:
origin:
x: -0.00300861804638783
y: -0.000514813312550808
@@ -76,7 +76,7 @@ link3:
yy: 0.00114018992179734
yz: -1.06260802872572e-05
zz: 0.000559765478676303
-link4:
+link5:
origin:
x: -0.00310326243752606
y: -0.00136538979048502
@@ -92,7 +92,7 @@ link4:
yy: 0.000433926270522842
yz: 3.41757797773822e-06
zz: 0.000245211429759716
-link5:
+link6:
origin:
x: -0.000737188580130584
y: -0.000412406544351845
@@ -108,7 +108,7 @@ link5:
yy: 0.000334599623120095
yz: -1.49934409279756e-07
zz: 0.000176662464388138
-link6:
+link7:
origin:
x: 2.27438423593244e-05
y: 0.00068649602098908
diff --git a/assets/robot/openarm_v2.0/config/arm/link/collisions.yaml b/assets/robot/openarm_v2.0/config/arm/link/collisions.yaml
index 288633ba..4980bbc4 100644
--- a/assets/robot/openarm_v2.0/config/arm/link/collisions.yaml
+++ b/assets/robot/openarm_v2.0/config/arm/link/collisions.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-base_link:
+link1:
origin:
x: 0
y: 0
@@ -26,7 +26,7 @@ base_link:
x: 1
y: 1
z: 1
-link1:
+link2:
origin:
x: 0
y: 0
@@ -40,7 +40,7 @@ link1:
x: 1
y: 1
z: 1
-link2:
+link3:
origin:
x: 0
y: 0
@@ -54,7 +54,7 @@ link2:
x: 1
y: 1
z: 1
-link3:
+link4:
origin:
x: 0
y: 0
@@ -68,7 +68,7 @@ link3:
x: 1
y: 1
z: 1
-link4:
+link5:
origin:
x: 0
y: 0
@@ -82,7 +82,7 @@ link4:
x: 1
y: 1
z: 1
-link5:
+link6:
origin:
x: 0
y: 0
@@ -96,7 +96,7 @@ link5:
x: 1
y: 1
z: 1
-link6:
+link7:
origin:
x: 0
y: 0
diff --git a/assets/robot/openarm_v2.0/config/arm/link/visuals.yaml b/assets/robot/openarm_v2.0/config/arm/link/visuals.yaml
index 6fcf1682..9e6a8467 100644
--- a/assets/robot/openarm_v2.0/config/arm/link/visuals.yaml
+++ b/assets/robot/openarm_v2.0/config/arm/link/visuals.yaml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-base_link:
+link1:
origin:
x: 0
y: 0
@@ -26,7 +26,7 @@ base_link:
x: 1
y: 1
z: 1
-link1:
+link2:
origin:
x: 0
y: 0
@@ -40,7 +40,7 @@ link1:
x: 1
y: 1
z: 1
-link2:
+link3:
origin:
x: 0
y: 0
@@ -54,7 +54,7 @@ link2:
x: 1
y: 1
z: 1
-link3:
+link4:
origin:
x: 0
y: 0
@@ -68,7 +68,7 @@ link3:
x: 1
y: 1
z: 1
-link4:
+link5:
origin:
x: 0
y: 0
@@ -82,7 +82,7 @@ link4:
x: 1
y: 1
z: 1
-link5:
+link6:
origin:
x: 0
y: 0
@@ -96,7 +96,7 @@ link5:
x: 1
y: 1
z: 1
-link6:
+link7:
origin:
x: 0
y: 0
diff --git a/assets/robot/openarm_v2.0/config/arm/struct/name_mapping.yaml b/assets/robot/openarm_v2.0/config/arm/struct/name_mapping.yaml
index 98d02fd9..dbabebb3 100644
--- a/assets/robot/openarm_v2.0/config/arm/struct/name_mapping.yaml
+++ b/assets/robot/openarm_v2.0/config/arm/struct/name_mapping.yaml
@@ -14,23 +14,23 @@
links:
canonical_to_original:
- base_link: base_link
- link1: link1
- link2: link2
- link3: link3
- link4: link4
- link5: link5
- link6: link6
- link7: empty_link_for_J7
+ link1: base_link
+ link2: link1
+ link3: link2
+ link4: link3
+ link5: link4
+ link6: link5
+ link7: link6
+ link8: empty_link_for_J7
original_to_canonical:
- base_link: base_link
- link1: link1
- link2: link2
- link3: link3
- link4: link4
- link5: link5
- link6: link6
- empty_link_for_J7: link7
+ base_link: link1
+ link1: link2
+ link2: link3
+ link3: link4
+ link4: link5
+ link5: link6
+ link6: link7
+ empty_link_for_J7: link8
joints:
canonical_to_original:
joint1: joint1
diff --git a/assets/robot/openarm_v2.0/config/arm/struct/reference_points.yaml b/assets/robot/openarm_v2.0/config/arm/struct/reference_points.yaml
index 2f524870..1cd09d60 100644
--- a/assets/robot/openarm_v2.0/config/arm/struct/reference_points.yaml
+++ b/assets/robot/openarm_v2.0/config/arm/struct/reference_points.yaml
@@ -13,7 +13,7 @@
# limitations under the License.
ee_mount_point:
- parent: link7
+ parent: link8
origin:
x: 0.0205
y: 0
@@ -29,7 +29,7 @@ ee_mount_point:
type: fixed
parent_link:
original_name: empty_link_for_J7
- canonical_name: link7
+ canonical_name: link8
child_link:
original_name: empty_link_for_ee_mounting
canonical_name: null
diff --git a/assets/robot/openarm_v2.0/config/arm/struct/topology.yaml b/assets/robot/openarm_v2.0/config/arm/struct/topology.yaml
index acd91f4b..f2f248e3 100644
--- a/assets/robot/openarm_v2.0/config/arm/struct/topology.yaml
+++ b/assets/robot/openarm_v2.0/config/arm/struct/topology.yaml
@@ -13,42 +13,49 @@
# limitations under the License.
collapse_empty_end: true
-
links:
- - base_link
- - link1
- - link2
- - link3
- - link4
- - link5
- - link6
- - link7
+ link1:
+ name: base_link
+ link2:
+ name: link1
+ link3:
+ name: link2
+ link4:
+ name: link3
+ link5:
+ name: link4
+ link6:
+ name: link5
+ link7:
+ name: link6
+ link8:
+ name: link7
joints:
joint1:
- type: revolute
- parent: base_link
- child: link1
- joint2:
type: revolute
parent: link1
child: link2
- joint3:
+ joint2:
type: revolute
parent: link2
child: link3
- joint4:
+ joint3:
type: revolute
parent: link3
child: link4
- joint5:
+ joint4:
type: revolute
parent: link4
child: link5
- joint6:
+ joint5:
type: revolute
parent: link5
child: link6
- joint7:
+ joint6:
type: revolute
parent: link6
child: link7
+ joint7:
+ type: revolute
+ parent: link7
+ child: link8
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 3771f40b..2a18cc1b 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
@@ -60,7 +60,7 @@ primary_arm:
y: -1
z: 1
include:
- links: [base_link, link1, link2, link5, link6, link7]
+ links: [link1, link2, link3, link6, link7, link8]
joints: [joint1, joint2, joint3, joint5, joint6, joint7]
reference_points: [ee_mount_point]
limit_override_joints: {}
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 2b94a8ae..66f1af6e 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
@@ -60,7 +60,7 @@ primary_arm:
y: -1
z: 1
include:
- links: [base_link, link1, link2, link5, link6, link7]
+ links: [link1, link2, link3, link6, link7, link8]
joints: [joint1, joint2, joint3, joint5, joint6, joint7]
reference_points: [ee_mount_point]
limit_override_joints: {}
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 fbfa0663..b7146b04 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
@@ -61,7 +61,7 @@ primary_arm:
y: -1
z: 1
include:
- links: [base_link, link1, link2, link5, link6, link7]
+ links: [link1, link2, link3, link6, link7, link8]
joints: [joint1, joint2, joint3, joint5, joint6, joint7]
reference_points: [ee_mount_point]
limit_override_joints: {}
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..ed37d4a9 100644
--- a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
+++ b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
@@ -39,7 +39,7 @@ limitations under the License.
-
+
Date: Fri, 6 Nov 2026 19:24:41 +0900
Subject: [PATCH 2/2] Declare control systems using component keys and stable
joint indices
---
README.md | 2 +
assets/common/assembly.xacro | 59 +++----
assets/common/ros2_control.xacro | 129 ++++++++++++++
assets/component_loader.xacro | 3 +-
.../robot_presets/default_bimanual.yaml | 26 +++
.../config/robot_presets/left_arm.yaml | 12 ++
.../left_arm_with_pinch_gripper.yaml | 14 ++
.../config/robot_presets/right_arm.yaml | 12 ++
.../right_arm_with_pinch_gripper.yaml | 14 ++
.../robot/openarm_v2.0/urdf/example/v2.urdf | 16 --
.../openarm_v2.0/urdf/openarm_v20.urdf.xacro | 46 ++---
.../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 | 161 ++++++++++++++++++
test/test_models.py | 43 +++++
17 files changed, 465 insertions(+), 352 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/README.md b/README.md
index 7f82e7cf..65483f8c 100644
--- a/README.md
+++ b/README.md
@@ -51,3 +51,5 @@ Copyright 2025 Enactic, Inc.
All participation in the OpenArm project is governed by our
[Code of Conduct](CODE_OF_CONDUCT.md).
+
+Control partitions are declared under the preset's top-level `systems` mapping. Each system's `joints` maps component instance names to stable local joint keys with an `idx`. Each active joint requires a unique non-negative integer `idx` within its system; gaps are allowed. Joints are emitted in index order, with `idx` included as a joint parameter for runtime adapters. Disabled components are skipped, and final joint names come from the resolved component descriptions. Standard OpenArm hardware defaults are supplied by the model entry. Per-system hardware_plugin and driver parameters can override those defaults, and CAN launch arguments take precedence. `use_fake_hardware:=true` selects `mock_components/GenericSystem`. Optional `command_interfaces` and `state_interfaces` default to `[position, velocity, effort]`.
diff --git a/assets/common/assembly.xacro b/assets/common/assembly.xacro
index e24c8c6a..1398db64 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">
@@ -133,8 +131,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" />
-
+
@@ -169,20 +168,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">
@@ -228,22 +217,19 @@ limitations under the License.
-
-
-
-
+ params="component_api config_root robot_preset emit_grasp_frame:=false out_name:=component_descriptions">
@@ -268,9 +252,14 @@ limitations under the License.
components="${components}"
robot_preset="${robot_preset}" />
+
+
+
+
-
+
+
+
+
+
+
+
+
+ ${values[keys[index]]}
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+
+
+
+
+ ${joint_indices[index]}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${plugin}
+
+
+
+
+
+
+
+
+
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/config/robot_presets/default_bimanual.yaml b/assets/robot/openarm_v2.0/config/robot_presets/default_bimanual.yaml
index 2a18cc1b..559c7625 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
@@ -118,3 +118,29 @@ secondary_arm_end_effector:
y: 1
z: 1
limit_override_joints: {}
+
+systems:
+ openarm_left_hardware_interface:
+ joints:
+ primary_arm:
+ joint1: {idx: 1}
+ joint2: {idx: 2}
+ joint3: {idx: 3}
+ joint4: {idx: 4}
+ joint5: {idx: 5}
+ joint6: {idx: 6}
+ joint7: {idx: 7}
+ primary_arm_end_effector:
+ joint1: {idx: 8}
+ openarm_right_hardware_interface:
+ joints:
+ secondary_arm:
+ joint1: {idx: 1}
+ joint2: {idx: 2}
+ joint3: {idx: 3}
+ joint4: {idx: 4}
+ joint5: {idx: 5}
+ joint6: {idx: 6}
+ joint7: {idx: 7}
+ secondary_arm_end_effector:
+ joint1: {idx: 8}
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 66f1af6e..b4debe9d 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
@@ -122,3 +122,15 @@ secondary_arm_end_effector:
y: 1
z: 1
limit_override_joints: {}
+
+systems:
+ openarm_left_hardware_interface:
+ joints:
+ primary_arm:
+ joint1: {idx: 1}
+ joint2: {idx: 2}
+ joint3: {idx: 3}
+ joint4: {idx: 4}
+ joint5: {idx: 5}
+ joint6: {idx: 6}
+ joint7: {idx: 7}
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 b7146b04..18fb9066 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
@@ -123,3 +123,17 @@ secondary_arm_end_effector:
y: 1
z: 1
limit_override_joints: {}
+
+systems:
+ openarm_left_hardware_interface:
+ joints:
+ primary_arm:
+ joint1: {idx: 1}
+ joint2: {idx: 2}
+ joint3: {idx: 3}
+ joint4: {idx: 4}
+ joint5: {idx: 5}
+ joint6: {idx: 6}
+ joint7: {idx: 7}
+ primary_arm_end_effector:
+ joint1: {idx: 8}
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 fe26dd88..d62cd67c 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
@@ -122,3 +122,15 @@ secondary_arm_end_effector:
y: -1
z: 1
limit_override_joints: {}
+
+systems:
+ openarm_right_hardware_interface:
+ joints:
+ primary_arm:
+ joint1: {idx: 1}
+ joint2: {idx: 2}
+ joint3: {idx: 3}
+ joint4: {idx: 4}
+ joint5: {idx: 5}
+ joint6: {idx: 6}
+ joint7: {idx: 7}
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 f5e60d0e..06936a6b 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
@@ -119,3 +119,17 @@ secondary_arm_end_effector:
y: -1
z: 1
limit_override_joints: {}
+
+systems:
+ openarm_right_hardware_interface:
+ joints:
+ primary_arm:
+ joint1: {idx: 1}
+ joint2: {idx: 2}
+ joint3: {idx: 3}
+ joint4: {idx: 4}
+ joint5: {idx: 5}
+ joint6: {idx: 6}
+ joint7: {idx: 7}
+ primary_arm_end_effector:
+ joint1: {idx: 8}
diff --git a/assets/robot/openarm_v2.0/urdf/example/v2.urdf b/assets/robot/openarm_v2.0/urdf/example/v2.urdf
index 39ad0c70..9ae8fcba 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 ed37d4a9..dee35f42 100644
--- a/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
+++ b/assets/robot/openarm_v2.0/urdf/openarm_v20.urdf.xacro
@@ -16,42 +16,42 @@ limitations under the License.
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
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 72facef0..f40ae610 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..5d79f5d1 100644
--- a/test/test_interfaces.py
+++ b/test/test_interfaces.py
@@ -67,3 +67,164 @@ 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_'),
+ tool=support.component('end_effector', 'pinch_gripper', 'openarm_right_', connection=dict(attach_to='arm')))
+ spec['systems'] = {'fixture_system': dict(
+ hardware_plugin='openarm_hardware/OpenArmHW', can_interface='can_test', can_fd=True,
+ joints=dict(arm={name: dict(idx=i) for i, name in enumerate(data['struct/topology.yaml']['joints'])},
+ tool=dict(joint1=dict(idx=8))))}
+ return spec, {'arm': data, 'tool': support.pinch()}
+
+
+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['joint7']['name'] = 'wrist'
+ selection = spec['systems']['fixture_system']['joints']['arm']
+ spec['systems']['fixture_system']['joints']['arm'] = dict(
+ reversed(list(selection.items())))
+ 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)
+ expected = {spec['arm']['prefix'] + cfg.get('name', name)
+ for name, cfg in arm_joints.items()}
+ if tool_enabled:
+ expected.update(spec['tool']['prefix'] + cfg.get('name', name)
+ for name, cfg in data['tool']['struct/topology.yaml']['joints'].items()
+ 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
+ expected_indices = {spec[component]['prefix'] + data[component]['struct/topology.yaml']['joints'][name].get('name', name): cfg['idx']
+ for component, joints in spec['systems']['fixture_system']['joints'].items()
+ if spec[component]['enabled'] for name, cfg in joints.items()}
+ actual_indices = {joint.get('name'): int(joint.find("param[@name='idx']").text)
+ for joint in robot.findall('ros2_control/joint')}
+ assert actual_indices == expected_indices
+ assert list(actual_indices.values()) == sorted(
+ expected_indices.values())
+
+ data['arm']['struct/topology.yaml']['collapse_empty_end'] = True
+ spec['tool']['connection']['parent_reference_point'] = 'port'
+ spec['systems']['fixture_system']['joints']['tool']['joint2'] = dict(
+ idx=9)
+ 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
+
+ spec, data = control_input()
+ joints = list(data['arm']['struct/topology.yaml']['joints'])
+ spec['systems'] = {
+ 'shoulder': dict(joints=dict(arm={name: dict(idx=2*i-1) for i, name in enumerate(joints[:4], 1)})),
+ 'wrist': dict(joints=dict(arm={name: dict(idx=2*i-1) for i, name in enumerate(joints[4:], 1)},
+ tool=dict(joint1=dict(idx=7))))}
+ robot = support.assemble(
+ spec, data, ros2_control=True, use_fake_hardware=True)
+ for name, cfg in spec['systems'].items():
+ expected = {spec[component]['prefix'] + data[component]['struct/topology.yaml']['joints'][joint].get('name', joint) for component,
+ names in cfg['joints'].items() for joint in names}
+ assert {joint.get('name') for joint in robot.find(
+ f"ros2_control[@name='{name}']").findall('joint')} == expected
+ spec['systems']['shoulder'].update(
+ hardware_plugin='openarm_hardware/OpenArmHW', can_interface='can_test')
+ with pytest.raises(xacro.XacroException, match='seven arm joints'):
+ support.assemble(spec, data, ros2_control=True,
+ use_fake_hardware=False)
+ spec['systems']['wrist']['joints']['arm'][joints[0]] = dict(idx=9)
+ with pytest.raises(xacro.XacroException, match='duplicate joint assignment'):
+ support.assemble(spec, data, ros2_control=True, use_fake_hardware=True)
+ spec['systems']['wrist']['joints']['arm']['missing'] = spec['systems']['wrist']['joints']['arm'].pop(
+ joints[0])
+ with pytest.raises(xacro.XacroException, match='unknown joint'):
+ support.assemble(spec, data, ros2_control=True, use_fake_hardware=True)
+ del spec['systems']['wrist']['joints']['arm']['missing']
+ for invalid_idx in (-1, True, 1.5):
+ spec['systems']['wrist']['joints']['tool']['joint1']['idx'] = invalid_idx
+ with pytest.raises(xacro.XacroException, match='non-negative integer idx'):
+ support.assemble(spec, data, ros2_control=True,
+ use_fake_hardware=True)
+ spec['systems']['wrist']['joints']['tool']['joint1']['idx'] = 1
+ with pytest.raises(xacro.XacroException, match='duplicate idx'):
+ support.assemble(spec, data, ros2_control=True, use_fake_hardware=True)
+
+
+def test_hardware_parameters_follow_input():
+ spec, data = control_input()
+ control = spec['systems']['fixture_system']
+ for prefix, can_interface in [('openarm_left_', 'can_left_test'),
+ ('openarm_right_', 'can_right_test')]:
+ control.update(can_interface=can_interface, can_fd=False)
+ for name in ('arm', 'tool'):
+ 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
+
+ for side in ('left', 'right'):
+ spec['systems'] = {f'openarm_{side}_hardware_interface': control}
+ control.update(can_fd=True)
+ overrides = dict(left_can_interface='runtime_left',
+ right_can_interface='runtime_right', can_fd=False)
+ robot = support.assemble(spec, data, ros2_control=True,
+ use_fake_hardware=False, **overrides)
+ hardware = robot.find('ros2_control/hardware')
+ params = {p.get('name'): p.text for p in hardware.findall('param')}
+ assert params['can_interface'] == overrides[side + '_can_interface']
+ assert params['can_fd'].lower() == 'false'
+
+ 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
+
+ control.update(command_interfaces=[
+ 'position'], state_interfaces=['position'])
+ robot = support.assemble(
+ spec, data, ros2_control=True, use_fake_hardware=True)
+ for joint in robot.findall('ros2_control/joint'):
+ assert [i.get('name') for i in joint.findall(
+ 'command_interface')] == ['position']
+ assert [i.get('name')
+ for i in joint.findall('state_interface')] == ['position']
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)