# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# 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.
# ----------------------------------------------------------------------------
"""Asset and mounting specifications for W1 external end effectors."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from types import MappingProxyType
import numpy as np
from scipy.spatial.transform import Rotation as R
from .types import (
DexforceW1ArmSide,
DexforceW1HandBrand,
DexforceW1HandVersion,
)
__all__ = [
"W1HandSideSpec",
"W1HandSpec",
"get_default_w1_hand_version",
"get_w1_hand_spec",
"normalize_w1_hand_mappings",
]
def _attach_xpos(
rotation_xyz_degrees: tuple[float, float, float],
translation: tuple[float, float, float] = (0.0, 0.0, 0.0),
) -> tuple[tuple[float, ...], ...]:
transform = np.eye(4)
transform[:3, :3] = R.from_euler(
"xyz", rotation_xyz_degrees, degrees=True
).as_matrix()
transform[:3, 3] = translation
return tuple(tuple(float(value) for value in row) for row in transform)
@dataclass(frozen=True)
class W1HandSideSpec:
"""Side-specific asset metadata for one hand release."""
urdf_path: str
joint_names: tuple[str, ...]
end_link_name: str
root_link_name: str
attach_xpos: tuple[tuple[float, ...], ...]
@dataclass(frozen=True)
class W1HandSpec:
"""A hand release independent of the W1 body/arm version."""
brand: DexforceW1HandBrand
version: DexforceW1HandVersion
sides: Mapping[DexforceW1ArmSide, W1HandSideSpec]
def __post_init__(self) -> None:
object.__setattr__(self, "sides", MappingProxyType(dict(self.sides)))
def for_side(self, side: DexforceW1ArmSide | str) -> W1HandSideSpec:
side = DexforceW1ArmSide.parse(side)
try:
return self.sides[side]
except KeyError as exc:
raise ValueError(
f"Hand {self.brand.value} {self.version.value} has no "
f"{side.value} asset registered"
) from exc
[docs]
def normalize_w1_hand_mappings(
hand_types: Mapping | None = None,
hand_versions: Mapping | None = None,
hand_attach_xposes: Mapping | None = None,
) -> tuple[dict, dict, dict]:
"""Normalize optional side-keyed hand configuration mappings."""
fields = {
"hand_types": hand_types,
"hand_versions": hand_versions,
"hand_attach_xposes": hand_attach_xposes,
}
for field_name, value in fields.items():
if value is not None and not isinstance(value, Mapping):
raise TypeError(f"{field_name} must be a mapping or None")
normalized_types = {
DexforceW1ArmSide.parse(side): DexforceW1HandBrand.parse(brand)
for side, brand in (hand_types or {}).items()
}
normalized_versions = {
DexforceW1ArmSide.parse(side): DexforceW1HandVersion.parse(version)
for side, version in (hand_versions or {}).items()
}
normalized_attach_xposes = {
DexforceW1ArmSide.parse(side): np.asarray(transform, dtype=float)
for side, transform in (hand_attach_xposes or {}).items()
}
return normalized_types, normalized_versions, normalized_attach_xposes
def _brainco_side(side: DexforceW1ArmSide) -> W1HandSideSpec:
prefix = "LEFT" if side == DexforceW1ArmSide.LEFT else "RIGHT"
side_name = "Left" if side == DexforceW1ArmSide.LEFT else "Right"
rotation = (90.0, 0.0, 180.0 if side == DexforceW1ArmSide.LEFT else 0.0)
return W1HandSideSpec(
urdf_path=(
f"BrainCoHandRevo1/BrainCo{side_name}Hand/" f"BrainCo{side_name}Hand.urdf"
),
joint_names=(
f"{prefix}_HAND_THUMB1",
f"{prefix}_HAND_THUMB2",
f"{prefix}_HAND_INDEX",
f"{prefix}_HAND_MIDDLE",
f"{prefix}_HAND_RING",
f"{prefix}_HAND_PINKY",
),
end_link_name=f"{side.value}_hand_base",
root_link_name=f"{side.value}_thumb_dist",
attach_xpos=_attach_xpos(rotation),
)
def _dh_gripper_side(
side: DexforceW1ArmSide,
modified: bool,
) -> W1HandSideSpec:
prefix = "LEFT" if side == DexforceW1ArmSide.LEFT else "RIGHT"
suffix = "_M" if modified else ""
joint_suffixes = (
("FINGER1", "FINGER2")
if modified
else (
"FINGER1_JOINT",
"FINGER2_JOINT",
)
)
return W1HandSideSpec(
urdf_path=f"DH_PGC_140_50{suffix}/DH_PGC_140_50{suffix}.urdf",
joint_names=tuple(f"{prefix}_{name}" for name in joint_suffixes),
end_link_name=f"{side.value}_base_link_1",
root_link_name=(
f"{side.value}_finger2" if modified else f"{side.value}_finger2_link"
),
attach_xpos=_attach_xpos(
(0.0, 0.0, 90.0),
(0.0, 0.0, 0.0 if modified else 0.015),
),
)
_W1_HAND_SPECS = {
(
DexforceW1HandBrand.BRAINCO_HAND,
DexforceW1HandVersion.V021,
): W1HandSpec(
brand=DexforceW1HandBrand.BRAINCO_HAND,
version=DexforceW1HandVersion.V021,
sides={side: _brainco_side(side) for side in DexforceW1ArmSide},
),
(
DexforceW1HandBrand.DH_PGC_GRIPPER,
DexforceW1HandVersion.V021,
): W1HandSpec(
brand=DexforceW1HandBrand.DH_PGC_GRIPPER,
version=DexforceW1HandVersion.V021,
sides={side: _dh_gripper_side(side, False) for side in DexforceW1ArmSide},
),
(
DexforceW1HandBrand.DH_PGC_GRIPPER_M,
DexforceW1HandVersion.V021,
): W1HandSpec(
brand=DexforceW1HandBrand.DH_PGC_GRIPPER_M,
version=DexforceW1HandVersion.V021,
sides={side: _dh_gripper_side(side, True) for side in DexforceW1ArmSide},
),
}
_DEFAULT_W1_HAND_VERSIONS = MappingProxyType(
{
DexforceW1HandBrand.BRAINCO_HAND: DexforceW1HandVersion.V021,
DexforceW1HandBrand.DH_PGC_GRIPPER: DexforceW1HandVersion.V021,
DexforceW1HandBrand.DH_PGC_GRIPPER_M: DexforceW1HandVersion.V021,
}
)
[docs]
def get_default_w1_hand_version(
brand: DexforceW1HandBrand | str,
) -> DexforceW1HandVersion:
"""Return the explicitly selected default release for a hand brand."""
brand = DexforceW1HandBrand.parse(brand)
try:
return _DEFAULT_W1_HAND_VERSIONS[brand]
except KeyError as exc:
raise ValueError(
f"No default hand version registered for {brand.value}"
) from exc
[docs]
def get_w1_hand_spec(
brand: DexforceW1HandBrand | str,
version: DexforceW1HandVersion | str,
) -> W1HandSpec:
"""Return an explicitly registered hand release."""
brand = DexforceW1HandBrand.parse(brand)
version = DexforceW1HandVersion.parse(version)
try:
return _W1_HAND_SPECS[(brand, version)]
except KeyError as exc:
raise ValueError(
f"No hand asset registered for brand={brand.value}, "
f"version={version.value}"
) from exc