2136 lines
68 KiB
Python
2136 lines
68 KiB
Python
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
import math
|
|
from pathlib import PurePosixPath
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from robot_common.materials import BLACK_ALUMINUM_RGBA
|
|
from v1.assemblies.base_link import (
|
|
STS3250_BASE_PLATE_FLUSH_Y_OFFSET_MM as BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
)
|
|
|
|
|
|
EXTRUSION_2020_LENGTH_MM = 100.0
|
|
SECONDARY_EXTRUSION_2020_LENGTH_MM = 150.0
|
|
SECONDARY_EXTRUSION_EXTENSION_MM = SECONDARY_EXTRUSION_2020_LENGTH_MM - EXTRUSION_2020_LENGTH_MM
|
|
EXTRUSION_BRACKET_INSERTION_THICKNESS_MM = 1.6
|
|
HORN_YOKE_SWEEP_LIMIT_DEG = 135.0
|
|
HORN_YOKE_SWEEP_LIMIT_RAD = math.radians(HORN_YOKE_SWEEP_LIMIT_DEG)
|
|
BASE_PLATE_BOTTOM_LOCAL_Y_MM = -32.9752
|
|
ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM = 2.7308
|
|
KGF_CM_TO_NM = 0.0980665
|
|
|
|
# Mass and effort source values verified against online vendor/project data on
|
|
# 2026-04-27:
|
|
# - STS3250: https://www.feetechrc.com/en/562636.html
|
|
# - STS3215: https://www.feetechrc.com/525603.html
|
|
# - HFS5-2020: https://misumi.partcommunity.com/3d-cad-models/hfsmg5-2020-hfsfmg5-2020-hfs5-series-curved-extrusions-misumi
|
|
# - 5052-H32: https://sendcutsend.com/materials/5052-aluminum/
|
|
# - Robonine gripper: https://github.com/roboninecom/SO-ARM100-101-Parallel-Gripper
|
|
STS3250_MASS_KG = 0.0745
|
|
STS3215_MASS_KG = 0.055
|
|
STS3250_RATED_TORQUE_NM = 16.0 * KGF_CM_TO_NM
|
|
STS3215_RATED_TORQUE_NM = 10.0 * KGF_CM_TO_NM
|
|
STS3250_PEAK_STALL_TORQUE_NM = 50.0 * KGF_CM_TO_NM
|
|
STS3215_STALL_TORQUE_NM = 30.0 * KGF_CM_TO_NM
|
|
HFS5_2020_MASS_KG_PER_M = 0.5
|
|
HFS5_2020_100_MASS_KG = HFS5_2020_MASS_KG_PER_M * 0.100
|
|
HFS5_2020_150_MASS_KG = HFS5_2020_MASS_KG_PER_M * 0.150
|
|
ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 = 0.097 * 0.45359237 / (0.0254**3) / 1e9
|
|
BASE_PLATE_MASS_KG = 5745.825053237387 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3
|
|
SERVO_END_MOUNT_MASS_KG = 1934.588542178479 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3
|
|
SERVO_HORN_YOKE_MASS_KG = 4126.128540235039 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3
|
|
EXTRUSION_BRACKET_2020_MASS_KG = 2335.413008330946 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3
|
|
GRIPPER_ASSEMBLY_MASS_KG = 0.250
|
|
STS3250_JOINT_EFFORT_TEXT = f"{STS3250_RATED_TORQUE_NM:.6f}"
|
|
STS3215_JOINT_EFFORT_TEXT = f"{STS3215_RATED_TORQUE_NM:.6f}"
|
|
GRIPPER_SERVO_EFFORT_TEXT = f"{STS3215_STALL_TORQUE_NM:.6f}"
|
|
|
|
|
|
def _translate_instance_world_vector(
|
|
instance: dict[str, object],
|
|
*,
|
|
vector_xyz: tuple[float, float, float],
|
|
distance_mm: float,
|
|
) -> dict[str, object]:
|
|
transform = [float(value) for value in instance["transform"]]
|
|
transform[3] += vector_xyz[0] * distance_mm
|
|
transform[7] += vector_xyz[1] * distance_mm
|
|
transform[11] += vector_xyz[2] * distance_mm
|
|
return {**instance, "transform": transform}
|
|
|
|
|
|
def _instance_local_axis_world_vector(instance: dict[str, object], *, axis: str) -> tuple[float, float, float]:
|
|
transform = [float(value) for value in instance["transform"]]
|
|
return _transform_local_axis_world_vector(transform, axis=axis)
|
|
|
|
|
|
def _transform_local_axis_world_vector(transform: list[float], *, axis: str) -> tuple[float, float, float]:
|
|
axis_column = {"x": 0, "y": 1, "z": 2}[axis]
|
|
return (
|
|
transform[axis_column],
|
|
transform[4 + axis_column],
|
|
transform[8 + axis_column],
|
|
)
|
|
|
|
|
|
def _find_named_instance(instances: list[dict[str, object]], name: str) -> dict[str, object]:
|
|
for instance in instances:
|
|
if str(instance["name"]) == name:
|
|
return instance
|
|
raise RuntimeError(f"Missing assembly instance {name!r}")
|
|
|
|
|
|
def _translate_named_instances_world_vector(
|
|
instances: list[dict[str, object]],
|
|
names: frozenset[str],
|
|
*,
|
|
vector_xyz: tuple[float, float, float],
|
|
distance_mm: float,
|
|
) -> None:
|
|
for index, instance in enumerate(instances):
|
|
if str(instance["name"]) in names:
|
|
instances[index] = _translate_instance_world_vector(
|
|
instance,
|
|
vector_xyz=vector_xyz,
|
|
distance_mm=distance_mm,
|
|
)
|
|
|
|
|
|
def _replace_named_instance(
|
|
instances: list[dict[str, object]],
|
|
*,
|
|
name: str,
|
|
replacement: dict[str, object],
|
|
) -> None:
|
|
for index, instance in enumerate(instances):
|
|
if str(instance["name"]) == name:
|
|
instances[index] = replacement
|
|
return
|
|
raise RuntimeError(f"Missing assembly instance {name!r}")
|
|
|
|
|
|
def _base_ground_y_offset_mm() -> float:
|
|
base_plate = _find_named_instance(ASSEMBLY_INSTANCES, "base_plate")
|
|
base_plate_transform = [float(value) for value in base_plate["transform"]]
|
|
return -(base_plate_transform[7] + BASE_PLATE_BOTTOM_LOCAL_Y_MM)
|
|
|
|
|
|
def _grounded_top_level_transform(
|
|
transform: list[float],
|
|
*,
|
|
y_offset_mm: float = 0.0,
|
|
) -> list[float]:
|
|
grounded = [float(value) for value in transform]
|
|
grounded[7] += _base_ground_y_offset_mm() + y_offset_mm
|
|
return grounded
|
|
|
|
|
|
def _y_up_to_z_up_transform(transform: list[float]) -> list[float]:
|
|
# Rotate exported top-level assembly placements so old +Y becomes standard +Z.
|
|
source = [float(value) for value in transform]
|
|
return [
|
|
source[0], source[1], source[2], source[3],
|
|
-source[8], -source[9], -source[10], -source[11],
|
|
source[4], source[5], source[6], source[7],
|
|
0.0, 0.0, 0.0, 1.0,
|
|
]
|
|
|
|
|
|
def _standard_ground_top_level_transform(
|
|
transform: list[float],
|
|
*,
|
|
y_offset_mm: float = 0.0,
|
|
) -> list[float]:
|
|
return _y_up_to_z_up_transform(
|
|
_grounded_top_level_transform(transform, y_offset_mm=y_offset_mm)
|
|
)
|
|
|
|
|
|
def _translate_transform_world_offset(
|
|
transform: list[float],
|
|
offset_xyz_mm: tuple[float, float, float],
|
|
) -> list[float]:
|
|
translated = [float(value) for value in transform]
|
|
translated[3] += offset_xyz_mm[0]
|
|
translated[7] += offset_xyz_mm[1]
|
|
translated[11] += offset_xyz_mm[2]
|
|
return translated
|
|
|
|
|
|
def _insert_named_instance_before(
|
|
instances: list[dict[str, object]],
|
|
*,
|
|
before_name: str,
|
|
instance: dict[str, object],
|
|
) -> None:
|
|
for index, current in enumerate(instances):
|
|
if str(current["name"]) == before_name:
|
|
instances.insert(index, instance)
|
|
return
|
|
raise RuntimeError(f"Missing assembly instance {before_name!r}")
|
|
|
|
|
|
def _extrusion_bracket_transform_from_extrusion(
|
|
extrusion_instance: dict[str, object],
|
|
) -> list[float]:
|
|
transform = [float(value) for value in extrusion_instance["transform"]]
|
|
# The wrap bracket uses the extrusion's frame, flipped 180 degrees around its
|
|
# local x axis so the horn plate sits at the extrusion start plane.
|
|
for index in (1, 5, 9, 2, 6, 10):
|
|
transform[index] = -transform[index]
|
|
return transform
|
|
|
|
|
|
def _extrusion_end_bracket_transform_from_extrusion(
|
|
extrusion_instance: dict[str, object],
|
|
*,
|
|
length_mm: float,
|
|
) -> list[float]:
|
|
transform = [float(value) for value in extrusion_instance["transform"]]
|
|
# Match the extrusion frame at the downstream end plane so the bracket wraps
|
|
# the far end without rotating the existing downstream chain.
|
|
extrusion_z_world = _instance_local_axis_world_vector(extrusion_instance, axis="z")
|
|
transform[3] += extrusion_z_world[0] * -length_mm
|
|
transform[7] += extrusion_z_world[1] * -length_mm
|
|
transform[11] += extrusion_z_world[2] * -length_mm
|
|
return transform
|
|
|
|
|
|
ASSEMBLY_INSTANCES = [{'path': 'imports/sts3250.step',
|
|
'name': 'sts3250_1',
|
|
'transform': [0.0,
|
|
1.0,
|
|
0.0,
|
|
-1.264258,
|
|
-1.0,
|
|
0.0,
|
|
0.0,
|
|
52.610399203721,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'servo_horn_yoke.step',
|
|
'name': 'servo_horn_yoke',
|
|
'transform': [0.0,
|
|
1.0,
|
|
0.0,
|
|
-1.264258,
|
|
-1.0,
|
|
0.0,
|
|
0.0,
|
|
52.610399203721,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/sts3250.step',
|
|
'name': 'sts3250_2',
|
|
'transform': [1.0,
|
|
0.0,
|
|
0.0,
|
|
15.135742000006,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
134.620798778311,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'extrusion_bracket_2020.step',
|
|
'name': 'secondary_servo_2020_connector',
|
|
'transform': [1.0,
|
|
0.0,
|
|
0.0,
|
|
-10.364257999994,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
145.850798778311,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/hfs5_2020_150.step',
|
|
'name': 'secondary_servo_hfs5_2020',
|
|
'transform': [1.0,
|
|
0.0,
|
|
0.0,
|
|
-10.364257999994,
|
|
0.0,
|
|
0.0,
|
|
-1.0,
|
|
147.450798778311,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'servo_end_mount.step',
|
|
'name': 'quaternary_servo_end_mount',
|
|
'transform': [0.0,
|
|
1.0,
|
|
0.0,
|
|
-0.764257999994,
|
|
-1.0,
|
|
0.0,
|
|
0.0,
|
|
261.060997778311,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/sts3250.step',
|
|
'name': 'sts3250_4',
|
|
'transform': [0.0,
|
|
1.0,
|
|
0.0,
|
|
-0.764257999994,
|
|
-1.0,
|
|
0.0,
|
|
0.0,
|
|
261.060997778311,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'servo_horn_yoke.step',
|
|
'name': 'quinary_horn_yoke',
|
|
'transform': [-0.000376789973259,
|
|
0.999999858029296,
|
|
0.000376790000006,
|
|
-0.777330838358056,
|
|
-1.41970704e-07,
|
|
0.000376789973259,
|
|
-0.999999929014646,
|
|
286.55752815855016,
|
|
-0.999999929014646,
|
|
-0.000376790000006,
|
|
0.0,
|
|
-25.496532190119495,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/sts3215.step',
|
|
'name': 'sts3215_5',
|
|
'transform': [0.999999858029296,
|
|
0.000376789973259,
|
|
0.000376790000006,
|
|
15.653567529745356,
|
|
0.000376789973259,
|
|
1.41970704e-07,
|
|
-0.999999929014646,
|
|
286.56371915718586,
|
|
-0.000376790000006,
|
|
0.999999929014646,
|
|
0.0,
|
|
56.5076826323431,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'extrusion_bracket_2020.step',
|
|
'name': 'quinary_servo_2020_connector',
|
|
'transform': [0.999999858029296,
|
|
0.000376789973259,
|
|
0.000376790000006,
|
|
-9.838731499094065,
|
|
0.000376789973259,
|
|
1.41970704e-07,
|
|
-0.999999929014646,
|
|
286.55411391315283,
|
|
-0.000376790000006,
|
|
0.999999929014646,
|
|
0.0,
|
|
67.74728867422357,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/hfs5_2020_100.step',
|
|
'name': 'quinary_servo_hfs5_2020',
|
|
'transform': [0.999999858029296,
|
|
0.000376790000006,
|
|
-0.000376789973259,
|
|
-9.83812863513685,
|
|
0.000376789973259,
|
|
-0.999999929014646,
|
|
-1.41970704e-07,
|
|
286.55411414030596,
|
|
-0.000376790000006,
|
|
0.0,
|
|
-0.999999929014646,
|
|
69.347288560647,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'servo_end_mount.step',
|
|
'name': 'senary_servo_end_mount',
|
|
'transform': [-0.000376789973259,
|
|
0.999999858029296,
|
|
0.000376790000006,
|
|
-0.198788813720378,
|
|
-1.41970704e-07,
|
|
0.000376789973259,
|
|
-0.999999929014646,
|
|
286.5577461474151,
|
|
-0.999999929014646,
|
|
-0.000376790000006,
|
|
0.0,
|
|
182.95386361794084,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/sts3215.step',
|
|
'name': 'sts3215_6',
|
|
'transform': [-0.000376789973259,
|
|
0.999999858029296,
|
|
0.000376790000006,
|
|
-0.198788813720378,
|
|
-1.41970704e-07,
|
|
0.000376789973259,
|
|
-0.999999929014646,
|
|
286.5577461474151,
|
|
-0.999999929014646,
|
|
-0.000376790000006,
|
|
0.0,
|
|
182.95386361794084,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'servo_horn_yoke.step',
|
|
'name': 'septenary_horn_yoke',
|
|
'transform': [-0.000376789973259,
|
|
0.999999858029296,
|
|
0.000376790000006,
|
|
-0.198788813720376,
|
|
-1.41970704e-07,
|
|
0.000376789973259,
|
|
-0.999999929014646,
|
|
286.5577461474151,
|
|
-0.999999929014646,
|
|
-0.000376790000006,
|
|
0.0,
|
|
182.95386361794084,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/sts3215.step',
|
|
'name': 'sts3215_7',
|
|
'transform': [0.999999858029296,
|
|
0.000376789973259,
|
|
0.000376790000006,
|
|
16.232109554383037,
|
|
0.000376789973259,
|
|
1.41970704e-07,
|
|
-0.999999929014646,
|
|
286.5639371460508,
|
|
-0.000376790000006,
|
|
0.999999929014646,
|
|
0.0,
|
|
264.9580784404034,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'servo_end_mount.step',
|
|
'name': 'servo_end_mount',
|
|
'transform': [0.0,
|
|
1.0,
|
|
0.0,
|
|
-1.264258,
|
|
-1.0,
|
|
0.0,
|
|
0.0,
|
|
52.610399203721,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'imports/sts3250.step',
|
|
'name': 'sts3250_3',
|
|
'transform': [1.0,
|
|
0.0,
|
|
0.0,
|
|
14.635742,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
27.7702,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'base_plate.step',
|
|
'name': 'base_plate',
|
|
'transform': [1.0,
|
|
0.0,
|
|
0.0,
|
|
14.635742,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
27.7702,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]},
|
|
{'path': 'gripper/rb9_01_061_000_gripper.step',
|
|
'name': 'parallel_gripper',
|
|
'transform': [0.999999929014645,
|
|
0.0,
|
|
0.00037679,
|
|
-9.277696917667,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
286.552253,
|
|
-0.00037679,
|
|
0.0,
|
|
0.999999929014645,
|
|
326.167694693849,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0]}]
|
|
|
|
REMOVED_ROBOT_ARM_INSTANCE_NAMES = frozenset()
|
|
SECONDARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES = frozenset(
|
|
{
|
|
"quaternary_servo_end_mount",
|
|
"sts3250_4",
|
|
"quinary_horn_yoke",
|
|
"sts3215_5",
|
|
"quinary_servo_2020_connector",
|
|
"quinary_servo_hfs5_2020",
|
|
"quinary_servo_end_bracket",
|
|
"senary_servo_end_mount",
|
|
"sts3215_6",
|
|
"septenary_horn_yoke",
|
|
"sts3215_7",
|
|
"parallel_gripper",
|
|
}
|
|
)
|
|
QUINARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES = frozenset(
|
|
{
|
|
"senary_servo_end_mount",
|
|
"sts3215_6",
|
|
"septenary_horn_yoke",
|
|
"sts3215_7",
|
|
"parallel_gripper",
|
|
}
|
|
)
|
|
|
|
for connector_name, extrusion_name in (
|
|
("secondary_servo_2020_connector", "secondary_servo_hfs5_2020"),
|
|
("quinary_servo_2020_connector", "quinary_servo_hfs5_2020"),
|
|
):
|
|
connector_instance = _find_named_instance(ASSEMBLY_INSTANCES, connector_name)
|
|
extrusion_instance = _find_named_instance(ASSEMBLY_INSTANCES, extrusion_name)
|
|
_replace_named_instance(
|
|
ASSEMBLY_INSTANCES,
|
|
name=connector_name,
|
|
replacement={
|
|
**connector_instance,
|
|
"path": "extrusion_bracket_2020.step",
|
|
"transform": _extrusion_bracket_transform_from_extrusion(extrusion_instance),
|
|
},
|
|
)
|
|
|
|
for bracket_name, extrusion_name, before_name in (
|
|
("secondary_servo_end_bracket", "secondary_servo_hfs5_2020", "quaternary_servo_end_mount"),
|
|
("quinary_servo_end_bracket", "quinary_servo_hfs5_2020", "senary_servo_end_mount"),
|
|
):
|
|
extrusion_instance = _find_named_instance(ASSEMBLY_INSTANCES, extrusion_name)
|
|
bracket_length_mm = (
|
|
SECONDARY_EXTRUSION_2020_LENGTH_MM
|
|
if extrusion_name == "secondary_servo_hfs5_2020"
|
|
else EXTRUSION_2020_LENGTH_MM
|
|
)
|
|
_insert_named_instance_before(
|
|
ASSEMBLY_INSTANCES,
|
|
before_name=before_name,
|
|
instance={
|
|
"path": "extrusion_bracket_2020.step",
|
|
"name": bracket_name,
|
|
"transform": _extrusion_end_bracket_transform_from_extrusion(
|
|
extrusion_instance,
|
|
length_mm=bracket_length_mm,
|
|
),
|
|
},
|
|
)
|
|
|
|
_translate_named_instances_world_vector(
|
|
ASSEMBLY_INSTANCES,
|
|
SECONDARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES,
|
|
vector_xyz=tuple(
|
|
-value
|
|
for value in _instance_local_axis_world_vector(
|
|
_find_named_instance(ASSEMBLY_INSTANCES, "secondary_servo_hfs5_2020"),
|
|
axis="z",
|
|
)
|
|
),
|
|
distance_mm=SECONDARY_EXTRUSION_EXTENSION_MM,
|
|
)
|
|
_translate_named_instances_world_vector(
|
|
ASSEMBLY_INSTANCES,
|
|
SECONDARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES,
|
|
vector_xyz=tuple(
|
|
-value
|
|
for value in _instance_local_axis_world_vector(
|
|
_find_named_instance(ASSEMBLY_INSTANCES, "secondary_servo_hfs5_2020"),
|
|
axis="z",
|
|
)
|
|
),
|
|
distance_mm=EXTRUSION_BRACKET_INSERTION_THICKNESS_MM,
|
|
)
|
|
_translate_named_instances_world_vector(
|
|
ASSEMBLY_INSTANCES,
|
|
QUINARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES,
|
|
vector_xyz=tuple(
|
|
-value
|
|
for value in _instance_local_axis_world_vector(
|
|
_find_named_instance(ASSEMBLY_INSTANCES, "quinary_servo_hfs5_2020"),
|
|
axis="z",
|
|
)
|
|
),
|
|
distance_mm=EXTRUSION_BRACKET_INSERTION_THICKNESS_MM,
|
|
)
|
|
|
|
# Robonine gripper uses a module 0.5, 26-tooth pinion and a 76 mm full stroke.
|
|
GRIPPER_CLAW_TRAVEL_M = 0.038
|
|
GRIPPER_SERVO_CLOSE_DEG = 334.96
|
|
GRIPPER_SERVO_CLOSE_RAD = math.radians(GRIPPER_SERVO_CLOSE_DEG)
|
|
GRIPPER_RACK_TRAVEL_PER_RAD_M = GRIPPER_CLAW_TRAVEL_M / GRIPPER_SERVO_CLOSE_RAD
|
|
GRIPPER_SERVO_VELOCITY_RAD_PER_S = 0.03 / GRIPPER_RACK_TRAVEL_PER_RAD_M
|
|
|
|
# Mesh-derived gripper inertials. The Robonine gripper source gives a
|
|
# 250 g assembly mass, but not per-link mass properties. These values
|
|
# distribute that mass over the generated gripper mesh volume estimates.
|
|
GRIPPER_STATIC_MESH_VOLUME_MM3 = 69789.45759247476
|
|
GRIPPER_SERVO_GEAR_MESH_VOLUME_MM3 = 1118.1904419098348
|
|
GRIPPER_CLAW_MESH_VOLUME_MM3 = 19529.73092604228
|
|
GRIPPER_MESH_VOLUME_SUM_MM3 = (
|
|
GRIPPER_STATIC_MESH_VOLUME_MM3
|
|
+ GRIPPER_SERVO_GEAR_MESH_VOLUME_MM3
|
|
+ (2.0 * GRIPPER_CLAW_MESH_VOLUME_MM3)
|
|
)
|
|
GRIPPER_MESH_DENSITY_KG_PER_MM3 = GRIPPER_ASSEMBLY_MASS_KG / GRIPPER_MESH_VOLUME_SUM_MM3
|
|
|
|
|
|
def _mesh_estimated_mass_kg(volume_mm3: float) -> float:
|
|
return volume_mm3 * GRIPPER_MESH_DENSITY_KG_PER_MM3
|
|
|
|
|
|
def _mesh_center_m(center_xyz_mm: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
return tuple(value * 0.001 for value in center_xyz_mm)
|
|
|
|
|
|
def _mesh_inertia_kg_m2(inertia_mm5: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
return tuple(value * GRIPPER_MESH_DENSITY_KG_PER_MM3 * 1e-6 for value in inertia_mm5)
|
|
|
|
|
|
URDF_LINK_INERTIALS = {
|
|
"base_link": {
|
|
"mass": STS3250_MASS_KG + BASE_PLATE_MASS_KG,
|
|
"origin_xyz": (-0.013065695, 0.018748416, -0.000001347),
|
|
"inertia": (2.00286469315e-05, 3.27226966744e-05, 3.64056286856e-05),
|
|
},
|
|
"shoulder_yaw_link": {
|
|
"mass": STS3250_MASS_KG + SERVO_END_MOUNT_MASS_KG,
|
|
"origin_xyz": (0.001212643, 0.046188702, -0.000001520),
|
|
"inertia": (2.19822684157e-05, 1.33477342495e-05, 2.72133948517e-05),
|
|
},
|
|
"shoulder_pitch_link": {
|
|
"mass": STS3250_MASS_KG + SERVO_HORN_YOKE_MASS_KG,
|
|
"origin_xyz": (-0.046864415, 0.009984643, -0.000001415),
|
|
"inertia": (2.61354708556e-05, 5.11017950126e-05, 6.8521371663e-05),
|
|
},
|
|
"shoulder_roll_link": {
|
|
"mass": (
|
|
STS3250_MASS_KG
|
|
+ SERVO_END_MOUNT_MASS_KG
|
|
+ HFS5_2020_150_MASS_KG
|
|
+ 2 * EXTRUSION_BRACKET_2020_MASS_KG
|
|
),
|
|
"origin_xyz": (0.000577873, 0.144167410, -0.000000724),
|
|
"inertia": (0.000595051077045, 2.80096804783e-05, 0.0006060283843),
|
|
},
|
|
"elbow_pitch_link": {
|
|
"mass": STS3215_MASS_KG + SERVO_HORN_YOKE_MASS_KG,
|
|
"origin_xyz": (0.000001711, 0.009998836, 0.046867544),
|
|
"inertia": (6.85659210126e-05, 5.11223026952e-05, 2.61742086504e-05),
|
|
},
|
|
"elbow_roll_link": {
|
|
"mass": (
|
|
STS3215_MASS_KG
|
|
+ SERVO_END_MOUNT_MASS_KG
|
|
+ HFS5_2020_100_MASS_KG
|
|
+ 2 * EXTRUSION_BRACKET_2020_MASS_KG
|
|
),
|
|
"origin_xyz": (0.000680967, 0.113970513, -0.000000851),
|
|
"inertia": (0.000292565038128, 2.38225119977e-05, 0.000301901347194),
|
|
},
|
|
"wrist_pitch_link": {
|
|
"mass": STS3215_MASS_KG + SERVO_HORN_YOKE_MASS_KG,
|
|
"origin_xyz": (-0.046864415, 0.009984643, -0.000001415),
|
|
"inertia": (2.61354708556e-05, 5.11017950126e-05, 6.8521371663e-05),
|
|
},
|
|
"gripper_base_link": {
|
|
"mass": _mesh_estimated_mass_kg(GRIPPER_STATIC_MESH_VOLUME_MM3),
|
|
"origin_xyz": _mesh_center_m((-0.0126298648, 15.5220681, -11.3782697)),
|
|
"inertia": _mesh_inertia_kg_m2((72377447.4, 94420670.5, 126870002.0)),
|
|
},
|
|
"gripper_servo_link": {
|
|
"mass": _mesh_estimated_mass_kg(GRIPPER_SERVO_GEAR_MESH_VOLUME_MM3),
|
|
"origin_xyz": _mesh_center_m((0.0000000329379318, -0.00000015359035, -0.668503477)),
|
|
"inertia": _mesh_inertia_kg_m2((28356.08228658, 28356.0947249, 43883.87103322)),
|
|
},
|
|
"gripper_right_claw_link": {
|
|
"mass": _mesh_estimated_mass_kg(GRIPPER_CLAW_MESH_VOLUME_MM3),
|
|
"origin_xyz": _mesh_center_m((45.56599565, -1.48478733, 22.03617984)),
|
|
"inertia": _mesh_inertia_kg_m2((10873354.67241446, 9338011.90258315, 5226795.70594811)),
|
|
},
|
|
"gripper_left_claw_link": {
|
|
"mass": _mesh_estimated_mass_kg(GRIPPER_CLAW_MESH_VOLUME_MM3),
|
|
"origin_xyz": _mesh_center_m((-45.56599565, 1.48478733, 22.03617984)),
|
|
"inertia": _mesh_inertia_kg_m2((10873354.67241446, 9338011.90258318, 5226795.70594813)),
|
|
},
|
|
}
|
|
|
|
|
|
def _urdf_numeric_text(value: float) -> str:
|
|
numeric = float(value)
|
|
if abs(numeric) < 0.5e-9:
|
|
numeric = 0.0
|
|
return f"{numeric:.9f}".rstrip("0").rstrip(".")
|
|
|
|
|
|
def _urdf_link_inertial(link_name: str) -> dict[str, object]:
|
|
inertial = URDF_LINK_INERTIALS[link_name]
|
|
ixx, iyy, izz = inertial["inertia"]
|
|
return {
|
|
"origin_xyz": " ".join(_urdf_numeric_text(value) for value in inertial["origin_xyz"]),
|
|
"origin_rpy": "0 0 0",
|
|
"mass": _urdf_numeric_text(float(inertial["mass"])),
|
|
"inertia": {
|
|
"ixx": _urdf_numeric_text(ixx),
|
|
"ixy": "0",
|
|
"ixz": "0",
|
|
"iyy": _urdf_numeric_text(iyy),
|
|
"iyz": "0",
|
|
"izz": _urdf_numeric_text(izz),
|
|
},
|
|
}
|
|
|
|
|
|
URDF_LINKS = (
|
|
{"name": "base_footprint"},
|
|
{
|
|
"name": "base_link",
|
|
"inertial": _urdf_link_inertial("base_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "sts3250_3",
|
|
"origin_xyz": "0 0.030575 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
{
|
|
"instance_name": "base_plate",
|
|
"origin_xyz": "0 0.0329752 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "shoulder_yaw_link",
|
|
"inertial": _urdf_link_inertial("shoulder_yaw_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "sts3250_1",
|
|
"origin_xyz": "0.0096 0.033940199 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
},
|
|
{
|
|
"instance_name": "servo_end_mount",
|
|
"origin_xyz": "0.0096 0.033940199 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "shoulder_pitch_link",
|
|
"inertial": _urdf_link_inertial("shoulder_pitch_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "servo_horn_yoke",
|
|
"origin_xyz": "0.0255 0.0091 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
{
|
|
"instance_name": "sts3250_2",
|
|
"origin_xyz": "-0.0594 0.0255 0",
|
|
"origin_rpy": "0 0 1.570796327",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "shoulder_roll_link",
|
|
"inertial": _urdf_link_inertial("shoulder_roll_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "secondary_servo_2020_connector",
|
|
"origin_xyz": "0 0.0190404 0",
|
|
"origin_rpy": "-1.570796327 0 0",
|
|
},
|
|
{
|
|
"instance_name": "secondary_servo_hfs5_2020",
|
|
"origin_xyz": "0 0.0190404 0",
|
|
"origin_rpy": "1.570796327 0 0",
|
|
},
|
|
{
|
|
"instance_name": "secondary_servo_end_bracket",
|
|
"origin_xyz": "0 0.1690404 0",
|
|
"origin_rpy": "1.570796327 0 0",
|
|
},
|
|
{
|
|
"instance_name": "quaternary_servo_end_mount",
|
|
"origin_xyz": "0.0096 0.186981399 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
},
|
|
{
|
|
"instance_name": "sts3250_4",
|
|
"origin_xyz": "0.0096 0.186981399 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "elbow_pitch_link",
|
|
"inertial": _urdf_link_inertial("elbow_pitch_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "quinary_horn_yoke",
|
|
"origin_xyz": "0.00000347 0.009086927 -0.025496532",
|
|
"origin_rpy": "-1.570796327 1.570419537 -1.570419537",
|
|
},
|
|
{
|
|
"instance_name": "sts3215_5",
|
|
"origin_xyz": "-0.000002722 0.025518914 0.059397282",
|
|
"origin_rpy": "1.570796327 0.00037679 1.571173117",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "elbow_roll_link",
|
|
"inertial": _urdf_link_inertial("elbow_roll_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "quinary_servo_2020_connector",
|
|
"origin_xyz": "0.000003466 0.0190404 0",
|
|
"origin_rpy": "-1.570796327 0 0",
|
|
},
|
|
{
|
|
"instance_name": "quinary_servo_hfs5_2020",
|
|
"origin_xyz": "0.000003466 0.0190404 0",
|
|
"origin_rpy": "1.570796327 0 0",
|
|
},
|
|
{
|
|
"instance_name": "quinary_servo_end_bracket",
|
|
"origin_xyz": "0.000003466 0.1190404 0",
|
|
"origin_rpy": "1.570796327 0 0",
|
|
},
|
|
{
|
|
"instance_name": "senary_servo_end_mount",
|
|
"origin_xyz": "0.0096 0.136981399 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
},
|
|
{
|
|
"instance_name": "sts3215_6",
|
|
"origin_xyz": "0.0096 0.136981399 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "wrist_pitch_link",
|
|
"inertial": _urdf_link_inertial("wrist_pitch_link"),
|
|
"visuals": (
|
|
{
|
|
"instance_name": "septenary_horn_yoke",
|
|
"origin_xyz": "0.0255 0.0091 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
{
|
|
"instance_name": "sts3215_7",
|
|
"origin_xyz": "-0.0594 0.0255 0",
|
|
"origin_rpy": "0 0 1.570796327",
|
|
},
|
|
),
|
|
},
|
|
{"name": "wrist_roll_link"},
|
|
{
|
|
"name": "gripper_base_link",
|
|
"inertial": _urdf_link_inertial("gripper_base_link"),
|
|
"visuals": (
|
|
{
|
|
"mesh_filename": "3MF/gripper_static.3mf",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "gripper_servo_link",
|
|
"inertial": _urdf_link_inertial("gripper_servo_link"),
|
|
"visuals": (
|
|
{
|
|
"mesh_filename": "3MF/gripper_servo_gear.3mf",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "gripper_right_claw_link",
|
|
"inertial": _urdf_link_inertial("gripper_right_claw_link"),
|
|
"visuals": (
|
|
{
|
|
"mesh_filename": "3MF/gripper_right_claw.3mf",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
),
|
|
},
|
|
{
|
|
"name": "gripper_left_claw_link",
|
|
"inertial": _urdf_link_inertial("gripper_left_claw_link"),
|
|
"visuals": (
|
|
{
|
|
"mesh_filename": "3MF/gripper_left_claw.3mf",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
},
|
|
),
|
|
},
|
|
)
|
|
|
|
URDF_JOINTS = (
|
|
{
|
|
"name": "base_footprint_to_base_link",
|
|
"joint_type": "fixed",
|
|
"parent": "base_footprint",
|
|
"child": "base_link",
|
|
"origin_xyz": "0.014635742 0 0",
|
|
"origin_rpy": "1.570796327 0 0",
|
|
},
|
|
{
|
|
"name": "base_yaw",
|
|
"joint_type": "continuous",
|
|
"parent": "base_link",
|
|
"child": "shoulder_yaw_link",
|
|
"origin_xyz": "-0.0255 0.021475 0",
|
|
"origin_rpy": "0 0 0",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"effort": STS3250_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "shoulder_pitch",
|
|
"joint_type": "revolute",
|
|
"parent": "shoulder_yaw_link",
|
|
"child": "shoulder_pitch_link",
|
|
"origin_xyz": "0.0005 0.059440199 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"lower": f"{-HORN_YOKE_SWEEP_LIMIT_RAD:.9f}",
|
|
"upper": f"{HORN_YOKE_SWEEP_LIMIT_RAD:.9f}",
|
|
"effort": STS3250_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "shoulder_roll",
|
|
"joint_type": "continuous",
|
|
"parent": "shoulder_pitch_link",
|
|
"child": "shoulder_roll_link",
|
|
"origin_xyz": "-0.0503 0 0",
|
|
"origin_rpy": "0 0 1.570796327",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"effort": STS3250_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "elbow_pitch",
|
|
"joint_type": "revolute",
|
|
"parent": "shoulder_roll_link",
|
|
"child": "elbow_pitch_link",
|
|
"origin_xyz": "0.0005 0.212481399 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"lower": f"{-HORN_YOKE_SWEEP_LIMIT_RAD:.9f}",
|
|
"upper": f"{HORN_YOKE_SWEEP_LIMIT_RAD:.9f}",
|
|
"effort": STS3250_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "elbow_roll",
|
|
"joint_type": "continuous",
|
|
"parent": "elbow_pitch_link",
|
|
"child": "elbow_roll_link",
|
|
"origin_xyz": "0.000006888 0.000015489 0.050306891",
|
|
"origin_rpy": "1.570796327 0.00037679 1.571173117",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"effort": STS3215_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "wrist_pitch",
|
|
"joint_type": "revolute",
|
|
"parent": "elbow_roll_link",
|
|
"child": "wrist_pitch_link",
|
|
"origin_xyz": "0.0005 0.162481399 0",
|
|
"origin_rpy": "0 0 -1.570796327",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"lower": f"{-HORN_YOKE_SWEEP_LIMIT_RAD:.9f}",
|
|
"upper": f"{HORN_YOKE_SWEEP_LIMIT_RAD:.9f}",
|
|
"effort": STS3215_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "wrist_roll",
|
|
"joint_type": "continuous",
|
|
"parent": "wrist_pitch_link",
|
|
"child": "wrist_roll_link",
|
|
"origin_xyz": "-0.0503 0 0",
|
|
"origin_rpy": "0 0 1.570796327",
|
|
"axis_xyz": "0 1 0",
|
|
"limit": {
|
|
"effort": STS3215_JOINT_EFFORT_TEXT,
|
|
"velocity": "4.000000",
|
|
},
|
|
},
|
|
{
|
|
"name": "wrist_roll_to_gripper_base",
|
|
"joint_type": "fixed",
|
|
"parent": "wrist_roll_link",
|
|
"child": "gripper_base_link",
|
|
"origin_xyz": "-0.00003287 0.0674104 0.000002072",
|
|
"origin_rpy": "-1.570796185 -0.00037679 0",
|
|
},
|
|
{
|
|
"name": "gripper_servo",
|
|
"joint_type": "revolute",
|
|
"parent": "gripper_base_link",
|
|
"child": "gripper_servo_link",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
"axis_xyz": "0 0 1",
|
|
"attrs": {"default_deg": "0"},
|
|
"limit": {
|
|
"lower": "0",
|
|
"upper": f"{GRIPPER_SERVO_CLOSE_RAD:.9f}",
|
|
"effort": GRIPPER_SERVO_EFFORT_TEXT,
|
|
"velocity": f"{GRIPPER_SERVO_VELOCITY_RAD_PER_S:.6f}",
|
|
},
|
|
},
|
|
{
|
|
"name": "gripper_right_claw_slide",
|
|
"joint_type": "prismatic",
|
|
"parent": "gripper_base_link",
|
|
"child": "gripper_right_claw_link",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
"axis_xyz": "-1 0 0",
|
|
"limit": {
|
|
"lower": "0",
|
|
"upper": f"{GRIPPER_CLAW_TRAVEL_M:.6f}",
|
|
"effort": "150",
|
|
"velocity": "0.03",
|
|
},
|
|
"mimic": {
|
|
"joint": "gripper_servo",
|
|
"multiplier": f"{GRIPPER_RACK_TRAVEL_PER_RAD_M:.12f}",
|
|
"offset": "0",
|
|
},
|
|
},
|
|
{
|
|
"name": "gripper_left_claw_slide",
|
|
"joint_type": "prismatic",
|
|
"parent": "gripper_base_link",
|
|
"child": "gripper_left_claw_link",
|
|
"origin_xyz": "0 0 0",
|
|
"origin_rpy": "0 0 0",
|
|
"axis_xyz": "1 0 0",
|
|
"limit": {
|
|
"lower": "0",
|
|
"upper": f"{GRIPPER_CLAW_TRAVEL_M:.6f}",
|
|
"effort": "150",
|
|
"velocity": "0.03",
|
|
},
|
|
"mimic": {
|
|
"joint": "gripper_servo",
|
|
"multiplier": f"{GRIPPER_RACK_TRAVEL_PER_RAD_M:.12f}",
|
|
"offset": "0",
|
|
},
|
|
},
|
|
)
|
|
|
|
URDF_VISUAL_INSTANCE_NAMES = tuple(
|
|
str(visual["instance_name"])
|
|
for link in URDF_LINKS
|
|
for visual in link.get("visuals", ())
|
|
if "instance_name" in visual and str(visual["instance_name"]) not in REMOVED_ROBOT_ARM_INSTANCE_NAMES
|
|
)
|
|
|
|
URDF_MATERIALS = {
|
|
"black_aluminum": BLACK_ALUMINUM_RGBA,
|
|
}
|
|
|
|
URDF_STEP_MATERIALS = {
|
|
"imports/sts3250.step": "black_aluminum",
|
|
"imports/sts3215.step": "black_aluminum",
|
|
}
|
|
|
|
PRESERVE_SOURCE_COLOR_STEP_PATHS = frozenset(
|
|
{
|
|
"imports/sts3250.step",
|
|
"imports/sts3215.step",
|
|
"gripper/rb9_01_061_000_gripper.step",
|
|
}
|
|
)
|
|
EXPLORER_POSES = (
|
|
{
|
|
"name": "home",
|
|
"joint_values": {
|
|
"base_yaw": 0,
|
|
"shoulder_pitch": 0,
|
|
"shoulder_roll": 0,
|
|
"elbow_pitch": 0,
|
|
"elbow_roll": 0,
|
|
"wrist_pitch": 0,
|
|
"wrist_roll": 0,
|
|
"gripper_servo": 0,
|
|
},
|
|
},
|
|
{
|
|
"name": "reach",
|
|
"joint_values": {
|
|
"base_yaw": 0,
|
|
"shoulder_pitch": 90,
|
|
"shoulder_roll": 0,
|
|
"elbow_pitch": -90,
|
|
"elbow_roll": 0,
|
|
"wrist_pitch": 0,
|
|
"wrist_roll": 0,
|
|
"gripper_servo": 90,
|
|
},
|
|
},
|
|
{
|
|
"name": "action",
|
|
"joint_values": {
|
|
"base_yaw": 95,
|
|
"shoulder_pitch": 32,
|
|
"shoulder_roll": 35,
|
|
"elbow_pitch": -20,
|
|
"elbow_roll": -103,
|
|
"wrist_pitch": 65,
|
|
"wrist_roll": -71,
|
|
"gripper_servo": 185,
|
|
},
|
|
},
|
|
)
|
|
ROBOT_ARM_LINK_ASSEMBLIES = (
|
|
{
|
|
"path": "assemblies/base_link.step",
|
|
"name": "base_link",
|
|
"anchor_instance_name": "base_plate",
|
|
"top_level_y_offset_mm": BASE_PLATE_BOTTOM_LOCAL_Y_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"sts3250_3",
|
|
"base_plate",
|
|
}
|
|
),
|
|
},
|
|
{
|
|
"path": "assemblies/shoulder_yaw_link.step",
|
|
"name": "shoulder_yaw_link",
|
|
"anchor_instance_name": "sts3250_1",
|
|
"top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"sts3250_1",
|
|
"servo_end_mount",
|
|
}
|
|
),
|
|
},
|
|
{
|
|
"path": "assemblies/pitch_link_sts3250.step",
|
|
"name": "shoulder_pitch_link",
|
|
"anchor_instance_name": "servo_horn_yoke",
|
|
"top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"servo_horn_yoke",
|
|
"sts3250_2",
|
|
}
|
|
),
|
|
},
|
|
{
|
|
"path": "assemblies/shoulder_roll_link.step",
|
|
"name": "shoulder_roll_link",
|
|
"anchor_instance_name": "secondary_servo_2020_connector",
|
|
"top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"secondary_servo_2020_connector",
|
|
"secondary_servo_hfs5_2020",
|
|
"secondary_servo_end_bracket",
|
|
"quaternary_servo_end_mount",
|
|
"sts3250_4",
|
|
}
|
|
),
|
|
},
|
|
{
|
|
"path": "assemblies/pitch_link_sts3215.step",
|
|
"name": "elbow_pitch_link",
|
|
"anchor_instance_name": "quinary_horn_yoke",
|
|
"top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"quinary_horn_yoke",
|
|
"sts3215_5",
|
|
}
|
|
),
|
|
},
|
|
{
|
|
"path": "assemblies/elbow_roll_link.step",
|
|
"name": "elbow_roll_link",
|
|
"anchor_instance_name": "quinary_servo_2020_connector",
|
|
"top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"quinary_servo_2020_connector",
|
|
"quinary_servo_hfs5_2020",
|
|
"quinary_servo_end_bracket",
|
|
"senary_servo_end_mount",
|
|
"sts3215_6",
|
|
}
|
|
),
|
|
},
|
|
{
|
|
"path": "assemblies/pitch_link_sts3215.step",
|
|
"name": "wrist_pitch_link",
|
|
"anchor_instance_name": "septenary_horn_yoke",
|
|
"top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
"instance_names": frozenset(
|
|
{
|
|
"septenary_horn_yoke",
|
|
"sts3215_7",
|
|
}
|
|
),
|
|
},
|
|
)
|
|
|
|
|
|
def _xml(tag: str, attrs: dict[str, object] | None = None, children: tuple[ET.Element, ...] = ()) -> ET.Element:
|
|
element = ET.Element(tag, {key: str(value) for key, value in (attrs or {}).items()})
|
|
for child in children:
|
|
element.append(child)
|
|
return element
|
|
|
|
|
|
def _urdf_inertial_element(inertial: dict[str, object]) -> ET.Element:
|
|
return _xml(
|
|
"inertial",
|
|
children=(
|
|
_xml("origin", {"xyz": inertial["origin_xyz"], "rpy": inertial["origin_rpy"]}),
|
|
_xml("mass", {"value": inertial["mass"]}),
|
|
_xml("inertia", dict(inertial["inertia"])),
|
|
),
|
|
)
|
|
|
|
|
|
def _urdf_mesh_filename_for_entry(entry: dict[str, object]) -> str:
|
|
mesh_filename = entry.get("mesh_filename")
|
|
if mesh_filename is None:
|
|
mesh_filename = _urdf_mesh_filename(str(entry["instance_name"]))
|
|
return str(mesh_filename)
|
|
|
|
|
|
def _urdf_mesh_geometry_element(entry: dict[str, object]) -> ET.Element:
|
|
mesh_filename = _urdf_mesh_filename_for_entry(entry)
|
|
return _xml(
|
|
"geometry",
|
|
children=(
|
|
_xml(
|
|
"mesh",
|
|
{
|
|
"filename": mesh_filename,
|
|
"scale": "0.001 0.001 0.001",
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def _urdf_visual_element(visual: dict[str, object]) -> ET.Element:
|
|
children = [
|
|
_xml("origin", {"xyz": visual["origin_xyz"], "rpy": visual["origin_rpy"]}),
|
|
_urdf_mesh_geometry_element(visual),
|
|
]
|
|
material_name = _urdf_visual_material_name(visual)
|
|
if material_name:
|
|
children.append(_xml("material", {"name": material_name}))
|
|
return _xml(
|
|
"visual",
|
|
children=tuple(children),
|
|
)
|
|
|
|
|
|
def _urdf_collision_element(collision: dict[str, object]) -> ET.Element:
|
|
return _xml(
|
|
"collision",
|
|
children=(
|
|
_xml("origin", {"xyz": collision["origin_xyz"], "rpy": collision["origin_rpy"]}),
|
|
_urdf_mesh_geometry_element(collision),
|
|
),
|
|
)
|
|
|
|
|
|
def _urdf_entry_is_removed(entry: dict[str, object]) -> bool:
|
|
return "instance_name" in entry and str(entry["instance_name"]) in REMOVED_ROBOT_ARM_INSTANCE_NAMES
|
|
|
|
|
|
def _urdf_link_element(link: dict[str, object]) -> ET.Element:
|
|
children: list[ET.Element] = []
|
|
inertial = link.get("inertial")
|
|
if inertial is not None:
|
|
children.append(_urdf_inertial_element(dict(inertial)))
|
|
for visual in link.get("visuals", ()):
|
|
if _urdf_entry_is_removed(dict(visual)):
|
|
continue
|
|
children.append(_urdf_visual_element(dict(visual)))
|
|
collision_specs = link.get("collisions")
|
|
if collision_specs is None:
|
|
collision_specs = link.get("visuals", ())
|
|
for collision in collision_specs:
|
|
if _urdf_entry_is_removed(dict(collision)):
|
|
continue
|
|
children.append(_urdf_collision_element(dict(collision)))
|
|
return _xml("link", {"name": link["name"]}, tuple(children))
|
|
|
|
|
|
def _urdf_joint_element(joint: dict[str, object]) -> ET.Element:
|
|
attrs = {"name": joint["name"], "type": joint["joint_type"]}
|
|
attrs.update(dict(joint.get("urdf_attrs", {})))
|
|
children = [
|
|
_xml("parent", {"link": joint["parent"]}),
|
|
_xml("child", {"link": joint["child"]}),
|
|
_xml("origin", {"xyz": joint["origin_xyz"], "rpy": joint["origin_rpy"]}),
|
|
]
|
|
axis_xyz = joint.get("axis_xyz")
|
|
if axis_xyz is not None:
|
|
children.append(_xml("axis", {"xyz": axis_xyz}))
|
|
limit = joint.get("limit")
|
|
if limit is not None:
|
|
children.append(_xml("limit", dict(limit)))
|
|
mimic = joint.get("mimic")
|
|
if mimic is not None:
|
|
children.append(_xml("mimic", dict(mimic)))
|
|
return _xml("joint", attrs, tuple(children))
|
|
|
|
|
|
def _explorer_numeric_value(value: float) -> float | int:
|
|
numeric = float(value)
|
|
return int(numeric) if numeric.is_integer() else numeric
|
|
|
|
|
|
def _urdf_explorer_metadata(
|
|
*,
|
|
joints: tuple[dict[str, object], ...] = URDF_JOINTS,
|
|
poses: tuple[dict[str, object], ...] = EXPLORER_POSES,
|
|
) -> dict[str, object]:
|
|
joint_defaults_by_name = {}
|
|
for joint in joints:
|
|
attrs = dict(joint.get("attrs", {}))
|
|
if "default_deg" in attrs:
|
|
joint_defaults_by_name[str(joint["name"])] = _explorer_numeric_value(float(attrs["default_deg"]))
|
|
return {
|
|
"schemaVersion": 3,
|
|
"kind": "texttocad-urdf-explorer",
|
|
"jointDefaultsByName": joint_defaults_by_name,
|
|
"poses": [
|
|
{
|
|
"name": str(pose["name"]),
|
|
"jointValuesByName": {
|
|
str(joint_name): _explorer_numeric_value(float(joint_value))
|
|
for joint_name, joint_value in dict(pose["joint_values"]).items()
|
|
},
|
|
}
|
|
for pose in poses
|
|
],
|
|
}
|
|
|
|
|
|
def _urdf_root_element(
|
|
*,
|
|
links: tuple[dict[str, object], ...] = URDF_LINKS,
|
|
joints: tuple[dict[str, object], ...] = URDF_JOINTS,
|
|
robot_name: str = "tom",
|
|
source_name: str = "tom.py",
|
|
) -> ET.Element:
|
|
root = _xml("robot", {"name": robot_name})
|
|
root.append(ET.Comment(f" Generated by {source_name} for CAD Explorer and external URDF consumers. "))
|
|
root.append(
|
|
ET.Comment(
|
|
" Link inertials use STS3250 mass 74.5 g and STS3215 mass 55 g from Feetech, HFS5-2020 profile "
|
|
"mass 0.5 kg/m from MISUMI, 5052-H32 density 0.097 lb/in^3 from "
|
|
"SendCutSend, and Robonine gripper assembly mass 250 g distributed "
|
|
"across generated gripper mesh volume estimates. Arm joint effort "
|
|
"uses each servo family's rated torque: STS3250 16 kg-cm "
|
|
"and STS3215 10 kg-cm at 12 V. "
|
|
)
|
|
)
|
|
used_material_names = sorted(
|
|
{
|
|
material_name
|
|
for link in links
|
|
for visual in link.get("visuals", ())
|
|
if (material_name := _urdf_visual_material_name(visual))
|
|
}
|
|
)
|
|
for material_name in used_material_names:
|
|
rgba = URDF_MATERIALS[material_name]
|
|
root.append(
|
|
_xml(
|
|
"material",
|
|
{"name": material_name},
|
|
(_xml("color", {"rgba": _urdf_rgba_text(rgba)}),),
|
|
)
|
|
)
|
|
for link in links:
|
|
root.append(_urdf_link_element(link))
|
|
root.append(
|
|
ET.Comment(
|
|
" Joint frames use the horn-center line resolved from "
|
|
"@cad[STEP/imports/sts3250#o1.4.f2,o1.5.f3]. "
|
|
)
|
|
)
|
|
for joint in joints:
|
|
root.append(_urdf_joint_element(joint))
|
|
return root
|
|
|
|
|
|
def _serialize_urdf(root: ET.Element) -> str:
|
|
ET.indent(root, space=" ")
|
|
return '<?xml version="1.0"?>\n' + ET.tostring(root, encoding="unicode")
|
|
|
|
|
|
def _assembly_instances() -> list[dict[str, object]]:
|
|
instances = []
|
|
for instance in ASSEMBLY_INSTANCES:
|
|
step_path = PurePosixPath(str(instance["path"])).as_posix()
|
|
instances.append(
|
|
{
|
|
"path": step_path,
|
|
"name": str(instance["name"]),
|
|
"transform": [float(value) for value in instance["transform"]],
|
|
"use_source_colors": step_path in PRESERVE_SOURCE_COLOR_STEP_PATHS,
|
|
}
|
|
)
|
|
return instances
|
|
|
|
|
|
def _assembly_children() -> list[dict[str, object]]:
|
|
source_instances = _assembly_instances()
|
|
source_by_name = {str(instance["name"]): instance for instance in source_instances}
|
|
missing_anchors = [
|
|
str(spec["anchor_instance_name"])
|
|
for spec in ROBOT_ARM_LINK_ASSEMBLIES
|
|
if str(spec["anchor_instance_name"]) not in source_by_name
|
|
]
|
|
if missing_anchors:
|
|
raise RuntimeError(f"Missing robot arm link assembly anchors: {missing_anchors!r}")
|
|
|
|
children = []
|
|
consumed_instance_names = set(REMOVED_ROBOT_ARM_INSTANCE_NAMES)
|
|
downstream_offset_xyz_mm = (0.0, 0.0, 0.0)
|
|
for link_spec in ROBOT_ARM_LINK_ASSEMBLIES:
|
|
anchor = source_by_name[str(link_spec["anchor_instance_name"])]
|
|
child_transform = _translate_transform_world_offset(
|
|
_standard_ground_top_level_transform(
|
|
anchor["transform"],
|
|
y_offset_mm=float(link_spec["top_level_y_offset_mm"]),
|
|
),
|
|
downstream_offset_xyz_mm,
|
|
)
|
|
children.append(
|
|
{
|
|
"path": str(link_spec["path"]),
|
|
"name": str(link_spec["name"]),
|
|
"transform": child_transform,
|
|
"use_source_colors": True,
|
|
}
|
|
)
|
|
consumed_instance_names.update(link_spec["instance_names"])
|
|
if str(link_spec["name"]) in {"shoulder_roll_link", "elbow_roll_link"}:
|
|
local_z_world = _transform_local_axis_world_vector(child_transform, axis="z")
|
|
downstream_offset_xyz_mm = (
|
|
downstream_offset_xyz_mm[0] + (local_z_world[0] * ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM),
|
|
downstream_offset_xyz_mm[1] + (local_z_world[1] * ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM),
|
|
downstream_offset_xyz_mm[2] + (local_z_world[2] * ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM),
|
|
)
|
|
|
|
for instance in source_instances:
|
|
instance_name = str(instance["name"])
|
|
if instance_name in consumed_instance_names:
|
|
continue
|
|
child_transform = _standard_ground_top_level_transform(
|
|
instance["transform"],
|
|
y_offset_mm=BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM,
|
|
)
|
|
if instance_name == "parallel_gripper":
|
|
child_transform = _translate_transform_world_offset(child_transform, downstream_offset_xyz_mm)
|
|
children.append(
|
|
{
|
|
**instance,
|
|
"transform": child_transform,
|
|
}
|
|
)
|
|
return children
|
|
|
|
|
|
def _assembly_instance_by_name() -> dict[str, dict[str, object]]:
|
|
instances = _assembly_instances()
|
|
by_name = {str(instance["name"]): instance for instance in instances}
|
|
if len(by_name) != len(instances):
|
|
raise RuntimeError("assembly instance names must be unique")
|
|
return by_name
|
|
|
|
|
|
def _urdf_rgba_text(rgba: tuple[float, float, float, float]) -> str:
|
|
return " ".join(f"{float(value):.6f}".rstrip("0").rstrip(".") for value in rgba)
|
|
|
|
|
|
def _urdf_visual_material_name(visual: dict[str, object]) -> str:
|
|
if str(_urdf_mesh_filename_for_entry(visual)).lower().endswith(".3mf"):
|
|
return ""
|
|
material_name = visual.get("material")
|
|
if material_name is not None:
|
|
return str(material_name)
|
|
if "instance_name" not in visual:
|
|
return ""
|
|
by_name = _assembly_instance_by_name()
|
|
instance_name = str(visual["instance_name"])
|
|
try:
|
|
instance = by_name[instance_name]
|
|
except KeyError as exc:
|
|
raise RuntimeError(f"URDF visual references missing assembly instance {instance_name!r}") from exc
|
|
step_path = PurePosixPath(str(instance["path"])).as_posix()
|
|
return URDF_STEP_MATERIALS.get(step_path, "")
|
|
|
|
|
|
def _urdf_mesh_filename(instance_name: str) -> str:
|
|
by_name = _assembly_instance_by_name()
|
|
try:
|
|
instance = by_name[instance_name]
|
|
except KeyError as exc:
|
|
raise RuntimeError(f"URDF visual references missing assembly instance {instance_name!r}") from exc
|
|
step_path = PurePosixPath(str(instance["path"]))
|
|
if step_path.suffix.lower() != ".step":
|
|
raise RuntimeError(f"URDF visual instance {instance_name!r} does not reference a .step file")
|
|
return f"3MF/{step_path.stem}.3mf"
|
|
|
|
|
|
ROBOT_ARM_MOTION_JOINT_NAMES = [
|
|
"base_yaw",
|
|
"shoulder_pitch",
|
|
"shoulder_roll",
|
|
"elbow_pitch",
|
|
"elbow_roll",
|
|
"wrist_pitch",
|
|
"wrist_roll",
|
|
]
|
|
|
|
ROBOT_ARM_MOTION_DISABLED_COLLISION_PAIRS = (
|
|
("wrist_pitch_link", "gripper_base_link"),
|
|
("elbow_pitch_link", "shoulder_roll_link"),
|
|
)
|
|
|
|
ROBOT_ARM_SRDF_INSPECTION_ARM_GROUP_STATE_DEG = {
|
|
"base_yaw": 45.0,
|
|
"shoulder_pitch": -27.0,
|
|
"shoulder_roll": -53.001,
|
|
"elbow_pitch": 3.0,
|
|
"elbow_roll": 29.969,
|
|
"wrist_pitch": 58.004,
|
|
"wrist_roll": -56.0,
|
|
}
|
|
|
|
ROBOT_ARM_SRDF_INSPECTION_MIRRORED_ARM_GROUP_STATE_DEG = {
|
|
"base_yaw": 45.0,
|
|
"shoulder_pitch": -27.0,
|
|
"shoulder_roll": 53.001,
|
|
"elbow_pitch": 3.0,
|
|
"elbow_roll": -29.969,
|
|
"wrist_pitch": 58.004,
|
|
"wrist_roll": 56.0,
|
|
}
|
|
|
|
ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG = (
|
|
(
|
|
"home",
|
|
{
|
|
"base_yaw": 0.0,
|
|
"shoulder_pitch": 0.0,
|
|
"shoulder_roll": 0.0,
|
|
"elbow_pitch": 0.0,
|
|
"elbow_roll": 0.0,
|
|
"wrist_pitch": 0.0,
|
|
"wrist_roll": 0.0,
|
|
},
|
|
),
|
|
(
|
|
"reach_forward",
|
|
{
|
|
"base_yaw": 0.0,
|
|
"shoulder_pitch": 60.0,
|
|
"shoulder_roll": 0.0,
|
|
"elbow_pitch": -90.0,
|
|
"elbow_roll": 0.0,
|
|
"wrist_pitch": 30.0,
|
|
"wrist_roll": 0.0,
|
|
},
|
|
),
|
|
(
|
|
"inspection",
|
|
ROBOT_ARM_SRDF_INSPECTION_ARM_GROUP_STATE_DEG,
|
|
),
|
|
(
|
|
"inspection_mirrored",
|
|
ROBOT_ARM_SRDF_INSPECTION_MIRRORED_ARM_GROUP_STATE_DEG,
|
|
),
|
|
)
|
|
|
|
ROBOT_ARM_SRDF_ARM_WITH_GRIPPER_GROUP_STATES_DEG = ()
|
|
|
|
ROBOT_ARM_SRDF_GRIPPER_GROUP_STATES_RAD = (
|
|
("open", {"gripper_servo": 0.0}),
|
|
("half_closed", {"gripper_servo": GRIPPER_SERVO_CLOSE_RAD * 0.5}),
|
|
("closed", {"gripper_servo": GRIPPER_SERVO_CLOSE_RAD}),
|
|
)
|
|
|
|
|
|
def _joint_degrees_to_radians(joint_values_deg: dict[str, float]) -> dict[str, float]:
|
|
return {
|
|
joint_name: math.radians(float(value_deg))
|
|
for joint_name, value_deg in joint_values_deg.items()
|
|
}
|
|
|
|
|
|
def _srdf_group_state_element(
|
|
*,
|
|
name: str,
|
|
group: str,
|
|
joint_values_rad: dict[str, float],
|
|
) -> ET.Element:
|
|
return _xml(
|
|
"group_state",
|
|
{"name": name, "group": group},
|
|
tuple(
|
|
_xml("joint", {"name": joint_name, "value": _urdf_numeric_text(value_rad)})
|
|
for joint_name, value_rad in joint_values_rad.items()
|
|
),
|
|
)
|
|
|
|
|
|
def _srdf_adjacent_collision_pairs() -> tuple[tuple[str, str], ...]:
|
|
pairs = []
|
|
seen: set[tuple[str, str]] = set()
|
|
for joint in URDF_JOINTS:
|
|
parent = str(joint["parent"])
|
|
child = str(joint["child"])
|
|
if "base_footprint" in {parent, child}:
|
|
continue
|
|
key = tuple(sorted((parent, child)))
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
pairs.append((parent, child))
|
|
return tuple(pairs)
|
|
|
|
|
|
def _srdf_root_element(*, robot_name: str = "tom") -> ET.Element:
|
|
root = _xml("robot", {"name": robot_name})
|
|
root.append(ET.Comment(" Generated by tom.py for MoveIt2 and CAD Viewer test poses. "))
|
|
root.append(
|
|
_xml(
|
|
"virtual_joint",
|
|
{
|
|
"name": "fixed_base",
|
|
"type": "fixed",
|
|
"parent_frame": "world",
|
|
"child_link": "base_footprint",
|
|
},
|
|
)
|
|
)
|
|
|
|
arm_group = _xml("group", {"name": "arm"})
|
|
arm_group.append(_xml("chain", {"base_link": "base_link", "tip_link": "wrist_roll_link"}))
|
|
root.append(arm_group)
|
|
|
|
tcp_group = _xml("group", {"name": "tcp"})
|
|
tcp_group.append(_xml("link", {"name": "gripper_base_link"}))
|
|
root.append(tcp_group)
|
|
|
|
gripper_group = _xml("group", {"name": "gripper"})
|
|
gripper_group.append(_xml("joint", {"name": "gripper_servo"}))
|
|
root.append(gripper_group)
|
|
|
|
arm_with_gripper_group = _xml("group", {"name": "arm_with_gripper"})
|
|
arm_with_gripper_group.append(_xml("group", {"name": "arm"}))
|
|
arm_with_gripper_group.append(_xml("group", {"name": "gripper"}))
|
|
root.append(arm_with_gripper_group)
|
|
|
|
root.append(
|
|
_xml(
|
|
"end_effector",
|
|
{
|
|
"name": "gripper_tcp",
|
|
"parent_link": "wrist_roll_link",
|
|
"group": "tcp",
|
|
"parent_group": "arm",
|
|
},
|
|
)
|
|
)
|
|
|
|
for name, joint_values_deg in ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG:
|
|
root.append(
|
|
_srdf_group_state_element(
|
|
name=name,
|
|
group="arm",
|
|
joint_values_rad=_joint_degrees_to_radians(joint_values_deg),
|
|
)
|
|
)
|
|
for name, joint_values_rad in ROBOT_ARM_SRDF_GRIPPER_GROUP_STATES_RAD:
|
|
root.append(
|
|
_srdf_group_state_element(
|
|
name=name,
|
|
group="gripper",
|
|
joint_values_rad=joint_values_rad,
|
|
)
|
|
)
|
|
for name, joint_values_deg in ROBOT_ARM_SRDF_ARM_WITH_GRIPPER_GROUP_STATES_DEG:
|
|
root.append(
|
|
_srdf_group_state_element(
|
|
name=name,
|
|
group="arm_with_gripper",
|
|
joint_values_rad=_joint_degrees_to_radians(joint_values_deg),
|
|
)
|
|
)
|
|
for link1, link2 in _srdf_adjacent_collision_pairs():
|
|
root.append(_xml("disable_collisions", {"link1": link1, "link2": link2, "reason": "Adjacent"}))
|
|
return root
|
|
|
|
ROBOT_ARM_2X_ARM_1_BASE_YAW_DEG = 90.0
|
|
ROBOT_ARM_2X_ARM_2_BASE_YAW_DEG = -90.0
|
|
ROBOT_ARM_2X_BASE_OFFSET_X_M = 0.395
|
|
ROBOT_ARM_2X_HOLDING_HANDS_POSE_JOINT_VALUES = {
|
|
"arm_1_base_yaw": 90,
|
|
"arm_1_shoulder_pitch": 0,
|
|
"arm_1_shoulder_roll": 0,
|
|
"arm_1_elbow_pitch": 90,
|
|
"arm_1_elbow_roll": 0,
|
|
"arm_1_wrist_pitch": 0,
|
|
"arm_1_wrist_roll": -24,
|
|
"arm_1_gripper_servo": 208,
|
|
"arm_2_base_yaw": -90,
|
|
"arm_2_shoulder_pitch": 0,
|
|
"arm_2_shoulder_roll": 0,
|
|
"arm_2_elbow_pitch": 90,
|
|
"arm_2_elbow_roll": 0,
|
|
"arm_2_wrist_pitch": 0,
|
|
"arm_2_wrist_roll": -45,
|
|
"arm_2_gripper_servo": 334.95,
|
|
}
|
|
|
|
ROBOT_ARM_2X_ARM_SPECS = (
|
|
{
|
|
"name": "arm_1",
|
|
"prefix": "arm_1_",
|
|
"base_origin_xyz": (-ROBOT_ARM_2X_BASE_OFFSET_X_M, 0.0, 0.0),
|
|
"base_origin_rpy": (0.0, 0.0, 0.0),
|
|
"default_joint_degrees": {"base_yaw": ROBOT_ARM_2X_ARM_1_BASE_YAW_DEG},
|
|
},
|
|
{
|
|
"name": "arm_2",
|
|
"prefix": "arm_2_",
|
|
"base_origin_xyz": (ROBOT_ARM_2X_BASE_OFFSET_X_M, 0.0, 0.0),
|
|
"base_origin_rpy": (0.0, 0.0, 0.0),
|
|
"default_joint_degrees": {"base_yaw": ROBOT_ARM_2X_ARM_2_BASE_YAW_DEG},
|
|
},
|
|
)
|
|
|
|
ROBOT_ARM_2X_MOTION_JOINT_NAMES = [
|
|
f"{spec['prefix']}{joint_name}"
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS
|
|
for joint_name in ROBOT_ARM_MOTION_JOINT_NAMES
|
|
]
|
|
|
|
|
|
def _urdf_xyz_values(text: str) -> tuple[float, float, float]:
|
|
values = tuple(float(value) for value in text.split())
|
|
if len(values) != 3:
|
|
raise RuntimeError(f"URDF xyz value must have exactly 3 components: {text!r}")
|
|
return values
|
|
|
|
|
|
def _urdf_xyz_text(values: tuple[float, float, float]) -> str:
|
|
return " ".join(_urdf_numeric_text(value) for value in values)
|
|
|
|
|
|
def _prefixed_robot_arm_link(link: dict[str, object], *, prefix: str) -> dict[str, object]:
|
|
prefixed = deepcopy(link)
|
|
prefixed["name"] = f"{prefix}{link['name']}"
|
|
return prefixed
|
|
|
|
|
|
def _prefixed_robot_arm_joint(
|
|
joint: dict[str, object],
|
|
*,
|
|
prefix: str,
|
|
base_origin_xyz: tuple[float, float, float],
|
|
base_origin_rpy: tuple[float, float, float],
|
|
default_joint_degrees: dict[str, float],
|
|
) -> dict[str, object]:
|
|
prefixed = deepcopy(joint)
|
|
parent_name = str(joint["parent"])
|
|
child_name = str(joint["child"])
|
|
joint_name = str(joint["name"])
|
|
prefixed["name"] = f"{prefix}{joint['name']}"
|
|
prefixed["parent"] = "base_footprint" if parent_name == "base_footprint" else f"{prefix}{parent_name}"
|
|
prefixed["child"] = f"{prefix}{child_name}"
|
|
if parent_name == "base_footprint":
|
|
prefixed["origin_xyz"] = _urdf_xyz_text(base_origin_xyz)
|
|
prefixed["origin_rpy"] = _urdf_xyz_text(base_origin_rpy)
|
|
if joint_name in default_joint_degrees:
|
|
attrs = dict(prefixed.get("attrs", {}))
|
|
attrs["default_deg"] = _urdf_numeric_text(float(default_joint_degrees[joint_name]))
|
|
prefixed["attrs"] = attrs
|
|
if "mimic" in prefixed:
|
|
mimic = dict(prefixed["mimic"])
|
|
mimic["joint"] = f"{prefix}{mimic['joint']}"
|
|
prefixed["mimic"] = mimic
|
|
return prefixed
|
|
|
|
|
|
def _robot_arm_2x_links() -> tuple[dict[str, object], ...]:
|
|
links = [{"name": "base_footprint"}]
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS:
|
|
prefix = str(spec["prefix"])
|
|
links.extend(
|
|
_prefixed_robot_arm_link(link, prefix=prefix)
|
|
for link in URDF_LINKS
|
|
if str(link["name"]) != "base_footprint"
|
|
)
|
|
return tuple(links)
|
|
|
|
|
|
def _robot_arm_2x_joints() -> tuple[dict[str, object], ...]:
|
|
joints = []
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS:
|
|
prefix = str(spec["prefix"])
|
|
base_origin_xyz = tuple(float(value) for value in spec["base_origin_xyz"])
|
|
base_origin_rpy = tuple(float(value) for value in spec["base_origin_rpy"])
|
|
default_joint_degrees = {
|
|
str(name): float(value)
|
|
for name, value in dict(spec.get("default_joint_degrees", {})).items()
|
|
}
|
|
joints.extend(
|
|
_prefixed_robot_arm_joint(
|
|
joint,
|
|
prefix=prefix,
|
|
base_origin_xyz=base_origin_xyz,
|
|
base_origin_rpy=base_origin_rpy,
|
|
default_joint_degrees=default_joint_degrees,
|
|
)
|
|
for joint in URDF_JOINTS
|
|
)
|
|
return tuple(joints)
|
|
|
|
|
|
def _explorer_pose_by_name(name: str) -> dict[str, object]:
|
|
for pose in EXPLORER_POSES:
|
|
if str(pose["name"]) == name:
|
|
return pose
|
|
raise RuntimeError(f"Missing explorer pose {name!r}")
|
|
|
|
|
|
def _robot_arm_2x_default_joint_values() -> dict[str, object]:
|
|
joint_values = {}
|
|
home_pose = _explorer_pose_by_name("home")
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS:
|
|
prefix = str(spec["prefix"])
|
|
base_yaw_degrees = float(dict(spec.get("default_joint_degrees", {})).get("base_yaw", 0.0))
|
|
joint_values.update(
|
|
{
|
|
f"{prefix}{joint_name}": (
|
|
float(joint_value) + base_yaw_degrees
|
|
if str(joint_name) == "base_yaw"
|
|
else joint_value
|
|
)
|
|
for joint_name, joint_value in dict(home_pose["joint_values"]).items()
|
|
}
|
|
)
|
|
return joint_values
|
|
|
|
|
|
def _robot_arm_2x_explorer_pose(
|
|
pose: dict[str, object],
|
|
) -> dict[str, object]:
|
|
joint_values = {}
|
|
pose_joint_values = dict(pose["joint_values"])
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS:
|
|
prefix = str(spec["prefix"])
|
|
base_yaw_degrees = float(dict(spec.get("default_joint_degrees", {})).get("base_yaw", 0.0))
|
|
joint_values.update(
|
|
{
|
|
f"{prefix}{joint_name}": (
|
|
float(joint_value) + base_yaw_degrees
|
|
if str(joint_name) == "base_yaw"
|
|
else joint_value
|
|
)
|
|
for joint_name, joint_value in pose_joint_values.items()
|
|
}
|
|
)
|
|
return {
|
|
"name": str(pose["name"]),
|
|
"joint_values": joint_values,
|
|
}
|
|
|
|
|
|
def _robot_arm_2x_explorer_poses() -> tuple[dict[str, object], ...]:
|
|
return (
|
|
*(_robot_arm_2x_explorer_pose(pose) for pose in EXPLORER_POSES),
|
|
{
|
|
"name": "holding_hands",
|
|
"joint_values": ROBOT_ARM_2X_HOLDING_HANDS_POSE_JOINT_VALUES,
|
|
},
|
|
)
|
|
|
|
|
|
def _prefixed_disabled_collision_pairs(*, prefix: str) -> list[list[str]]:
|
|
return [
|
|
[f"{prefix}{left}", f"{prefix}{right}"]
|
|
for left, right in ROBOT_ARM_MOTION_DISABLED_COLLISION_PAIRS
|
|
]
|
|
|
|
|
|
def _robot_arm_2x_disabled_collision_pairs() -> list[list[str]]:
|
|
pairs = []
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS:
|
|
pairs.extend(_prefixed_disabled_collision_pairs(prefix=str(spec["prefix"])))
|
|
return pairs
|
|
|
|
|
|
def _robot_arm_2x_motion_joint_names(*, prefix: str) -> list[str]:
|
|
return [f"{prefix}{joint_name}" for joint_name in ROBOT_ARM_MOTION_JOINT_NAMES]
|
|
|
|
|
|
def _robot_arm_2x_motion_planning_groups() -> list[dict[str, object]]:
|
|
return [
|
|
{
|
|
"name": str(spec["name"]),
|
|
"jointNames": _robot_arm_2x_motion_joint_names(prefix=str(spec["prefix"])),
|
|
}
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS
|
|
]
|
|
|
|
|
|
def _robot_arm_2x_default_motion_joint_values_rad(*, prefix: str) -> dict[str, float]:
|
|
default_joint_values = _robot_arm_2x_default_joint_values()
|
|
return {
|
|
name: math.radians(float(default_joint_values[name]))
|
|
for name in _robot_arm_2x_motion_joint_names(prefix=prefix)
|
|
}
|
|
|
|
|
|
def robot_arm_assembly_children() -> list[dict[str, object]]:
|
|
return _assembly_children()
|
|
|
|
|
|
def robot_arm_step_envelope() -> dict[str, object]:
|
|
return {
|
|
"children": robot_arm_assembly_children(),
|
|
}
|
|
|
|
|
|
def robot_arm_urdf_envelope(*, urdf_output: str = "tom.urdf") -> dict[str, object]:
|
|
return {
|
|
"xml": _serialize_urdf(_urdf_root_element()),
|
|
"urdf_output": urdf_output,
|
|
"explorer_metadata": _urdf_explorer_metadata(),
|
|
}
|
|
|
|
|
|
def robot_arm_2x_urdf_envelope(*, urdf_output: str = "robot_arm_2x.urdf") -> dict[str, object]:
|
|
links = _robot_arm_2x_links()
|
|
joints = _robot_arm_2x_joints()
|
|
return {
|
|
"xml": _serialize_urdf(
|
|
_urdf_root_element(
|
|
links=links,
|
|
joints=joints,
|
|
robot_name="robot_arm_2x",
|
|
source_name="robot_arm_2x_urdf.py",
|
|
)
|
|
),
|
|
"urdf_output": urdf_output,
|
|
"explorer_metadata": _urdf_explorer_metadata(
|
|
joints=joints,
|
|
poses=_robot_arm_2x_explorer_poses(),
|
|
),
|
|
}
|
|
|
|
|
|
def robot_arm_srdf_envelope(*, urdf: str = "tom.urdf") -> dict[str, object]:
|
|
return {
|
|
"xml": _serialize_urdf(_srdf_root_element()),
|
|
"urdf": urdf,
|
|
}
|
|
|
|
|
|
def robot_arm_motion_envelope(*, urdf: str = "tom.urdf") -> dict[str, object]:
|
|
return {
|
|
"urdf": urdf,
|
|
"provider": "moveit_py",
|
|
"commands": ["urdf.solvePose", "urdf.planToPose"],
|
|
"planningGroup": "arm",
|
|
"jointNames": ROBOT_ARM_MOTION_JOINT_NAMES,
|
|
"endEffectors": [
|
|
{
|
|
"name": "gripper",
|
|
"link": "gripper_base_link",
|
|
"frame": "base_link",
|
|
"parentLink": "wrist_roll_link",
|
|
"positionTolerance": 0.002,
|
|
}
|
|
],
|
|
"planner": {
|
|
"pipeline": "ompl",
|
|
"plannerId": "RRTConnectkConfigDefault",
|
|
"planningTime": 1.0,
|
|
},
|
|
"disabledCollisionPairs": [list(pair) for pair in ROBOT_ARM_MOTION_DISABLED_COLLISION_PAIRS],
|
|
"groupStates": [
|
|
{
|
|
"name": name,
|
|
"jointValuesByNameRad": {
|
|
joint_name: joint_values_rad[joint_name]
|
|
for joint_name in ROBOT_ARM_MOTION_JOINT_NAMES
|
|
},
|
|
}
|
|
for name, joint_values_deg in ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG
|
|
for joint_values_rad in (_joint_degrees_to_radians(joint_values_deg),)
|
|
],
|
|
}
|
|
|
|
|
|
def robot_arm_2x_motion_envelope(*, urdf: str = "robot_arm_2x.urdf") -> dict[str, object]:
|
|
default_group = str(ROBOT_ARM_2X_ARM_SPECS[0]["name"])
|
|
default_prefix = str(ROBOT_ARM_2X_ARM_SPECS[0]["prefix"])
|
|
return {
|
|
"urdf": urdf,
|
|
"provider": "moveit_py",
|
|
"commands": ["urdf.solvePose", "urdf.planToPose"],
|
|
"planningGroup": default_group,
|
|
"jointNames": _robot_arm_2x_motion_joint_names(prefix=default_prefix),
|
|
"planningGroups": _robot_arm_2x_motion_planning_groups(),
|
|
"endEffectors": [
|
|
{
|
|
"name": f"{spec['name']}_gripper",
|
|
"link": f"{spec['prefix']}gripper_base_link",
|
|
"frame": f"{spec['prefix']}base_link",
|
|
"parentLink": f"{spec['prefix']}wrist_roll_link",
|
|
"planningGroup": str(spec["name"]),
|
|
"jointNames": _robot_arm_2x_motion_joint_names(prefix=str(spec["prefix"])),
|
|
"positionTolerance": 0.002,
|
|
}
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS
|
|
],
|
|
"planner": {
|
|
"pipeline": "ompl",
|
|
"plannerId": "RRTConnectkConfigDefault",
|
|
"planningTime": 1.0,
|
|
},
|
|
"disabledCollisionPairs": _robot_arm_2x_disabled_collision_pairs(),
|
|
"groupStates": [
|
|
{
|
|
"name": "home",
|
|
"planningGroup": str(spec["name"]),
|
|
"jointValuesByNameRad": _robot_arm_2x_default_motion_joint_values_rad(prefix=str(spec["prefix"])),
|
|
}
|
|
for spec in ROBOT_ARM_2X_ARM_SPECS
|
|
],
|
|
}
|