"""Transmission models for ros2_control integration.
This module defines the mechanical coupling between actuators and joints,
handling gear ratios, offsets, and hardware interface mappings.
Transmission Categories:
- **Simple**: 1-to-1 mapping between an actuator and a joint.
- **Differential**: 2-to-2 mapping (e.g., wrist or differential drive).
- **Linkage**: Complex mappings like four-bar linkages.
- **Custom**: Extensible plugin-based transmissions.
"""
from __future__ import annotations
from collections import Counter
from collections.abc import Sequence
from dataclasses import dataclass, field, replace
from enum import StrEnum
from .._utils.string_utils import is_valid_name
from ..constants import (
EPSILON,
HW_IF_POSITION,
)
from ..exceptions import RobotValidationError, ValidationErrorCode
class TransmissionType(StrEnum):
"""Standard transmission types in ros2_control."""
SIMPLE = "transmission_interface/SimpleTransmission"
DIFFERENTIAL = "transmission_interface/DifferentialTransmission"
FOUR_BAR_LINKAGE = "transmission_interface/FourBarLinkageTransmission"
CUSTOM = "custom"
[docs]
@dataclass(frozen=True)
class TransmissionJoint:
"""Joint specification in a transmission."""
name: str
hardware_interfaces: Sequence[str] = field(default_factory=lambda: (HW_IF_POSITION,))
mechanical_reduction: float | None = 1.0
offset: float = 0.0
[docs]
def __post_init__(self) -> None:
"""Validate transmission joint."""
if not self.name:
raise RobotValidationError(
ValidationErrorCode.NAME_EMPTY,
"Transmission joint name cannot be empty",
target="JointName",
value=self.name,
)
if not self.hardware_interfaces:
raise RobotValidationError(
ValidationErrorCode.VALUE_EMPTY,
f"Transmission joint '{self.name}' must have at least one interface",
target="HardwareInterfaces",
value=self.name,
)
if self.mechanical_reduction is not None and abs(self.mechanical_reduction) < EPSILON:
raise RobotValidationError(
ValidationErrorCode.INVALID_VALUE,
f"Mechanical reduction for transmission joint '{self.name}' cannot be zero",
target="MechanicalReduction",
value=self.mechanical_reduction,
)
object.__setattr__(self, "hardware_interfaces", tuple(self.hardware_interfaces))
[docs]
def with_prefix(self, prefix: str) -> TransmissionJoint:
"""Create a new transmission joint with a prefixed name."""
return replace(self, name=f"{prefix}{self.name}")
[docs]
def normalized(self) -> TransmissionJoint:
"""Return a new transmission joint with sorted interfaces."""
return replace(self, hardware_interfaces=tuple(sorted(self.hardware_interfaces)))
[docs]
@dataclass(frozen=True)
class TransmissionActuator:
"""Actuator specification in a transmission."""
name: str
hardware_interfaces: Sequence[str] = field(default_factory=lambda: (HW_IF_POSITION,))
mechanical_reduction: float | None = 1.0
offset: float = 0.0
[docs]
def __post_init__(self) -> None:
"""Validate transmission actuator."""
if not self.name:
raise RobotValidationError(
ValidationErrorCode.NAME_EMPTY,
"Transmission actuator name cannot be empty",
target="ActuatorName",
value=self.name,
)
if not self.hardware_interfaces:
raise RobotValidationError(
ValidationErrorCode.VALUE_EMPTY,
f"Transmission actuator '{self.name}' must have at least one interface",
target="HardwareInterfaces",
value=self.name,
)
if self.mechanical_reduction is not None and abs(self.mechanical_reduction) < EPSILON:
raise RobotValidationError(
ValidationErrorCode.INVALID_VALUE,
f"Mechanical reduction for transmission actuator '{self.name}' cannot be zero",
target="MechanicalReduction",
value=self.mechanical_reduction,
)
object.__setattr__(self, "hardware_interfaces", tuple(self.hardware_interfaces))
[docs]
def with_prefix(self, prefix: str) -> TransmissionActuator:
"""Create a new transmission actuator with a prefixed name."""
return replace(self, name=f"{prefix}{self.name}")
[docs]
def normalized(self) -> TransmissionActuator:
"""Return a new transmission actuator with sorted interfaces."""
return replace(self, hardware_interfaces=tuple(sorted(self.hardware_interfaces)))
[docs]
@dataclass(frozen=True)
class Transmission:
"""Mechanical transmission mapping between joints and actuators.
Transmissions describe how mathematical joint states relate to physical
actuator commands, including hardware interface specifications for
ros2_control.
"""
name: str
type: str # Plugin name (e.g., TransmissionType enum or custom)
joints: Sequence[TransmissionJoint] = field(default_factory=tuple)
actuators: Sequence[TransmissionActuator] = field(default_factory=tuple)
# Additional parameters for complex transmissions
parameters: dict[str, str] = field(default_factory=dict)
[docs]
def __post_init__(self) -> None:
"""Validate transmission configuration."""
if not self.name:
raise RobotValidationError(
ValidationErrorCode.NAME_EMPTY,
"Transmission name cannot be empty",
target="TransmissionName",
value=self.name,
)
if not self.type:
raise RobotValidationError(
ValidationErrorCode.VALUE_EMPTY,
"Transmission type cannot be empty",
target="TransmissionType",
value=self.type,
)
# Validate naming convention
if not is_valid_name(self.name):
raise RobotValidationError(
ValidationErrorCode.INVALID_NAME,
"Invalid characters in transmission name",
target="TransmissionName",
value=self.name,
)
# Must have at least one joint and one actuator
if not self.joints:
raise RobotValidationError(
ValidationErrorCode.VALUE_EMPTY,
"Transmission must have at least one joint",
target="TransmissionJoints",
value=self.name,
)
if not self.actuators:
raise RobotValidationError(
ValidationErrorCode.VALUE_EMPTY,
"Transmission must have at least one actuator",
target="TransmissionActuators",
value=self.name,
)
# Specific constraints for standard transmission types
if self.type == TransmissionType.SIMPLE.value:
if len(self.joints) != 1 or len(self.actuators) != 1:
raise RobotValidationError(
ValidationErrorCode.INVALID_VALUE,
"Simple transmission must have exactly 1 joint and 1 actuator",
target="TransmissionComponents",
)
elif self.type in (
TransmissionType.DIFFERENTIAL.value,
TransmissionType.FOUR_BAR_LINKAGE.value,
) and (len(self.joints) != 2 or len(self.actuators) != 2):
raise RobotValidationError(
ValidationErrorCode.INVALID_VALUE,
f"{self.type} must have exactly 2 joints and 2 actuators",
target="TransmissionComponents",
)
# Check for duplicate joint names
joint_names = [j.name for j in self.joints]
duplicates = {name for name, count in Counter(joint_names).items() if count > 1}
if duplicates:
raise RobotValidationError(
ValidationErrorCode.DUPLICATE_NAME,
f"Duplicate joints in transmission: {duplicates}",
target="DuplicateJoints",
value=self.name,
)
# Check for duplicate actuator names
actuator_names = [a.name for a in self.actuators]
duplicates = {name for name, count in Counter(actuator_names).items() if count > 1}
if duplicates:
raise RobotValidationError(
ValidationErrorCode.DUPLICATE_NAME,
f"Duplicate actuators in transmission: {duplicates}",
target="DuplicateActuators",
value=self.name,
)
object.__setattr__(self, "joints", tuple(self.joints))
object.__setattr__(self, "actuators", tuple(self.actuators))
[docs]
def with_prefix(self, prefix: str) -> Transmission:
"""Create a new transmission with prefixed name, joints, and actuators."""
return replace(
self,
name=f"{prefix}{self.name}",
joints=tuple(j.with_prefix(prefix) for j in self.joints),
actuators=tuple(a.with_prefix(prefix) for a in self.actuators),
)
[docs]
def normalized(self) -> Transmission:
"""Return a new transmission with sorted joints and actuators."""
return replace(
self,
joints=tuple(sorted([j.normalized() for j in self.joints], key=lambda x: x.name)),
actuators=tuple(sorted([a.normalized() for a in self.actuators], key=lambda x: x.name)),
)
[docs]
@classmethod
def create_simple(
cls,
name: str,
joint_name: str,
actuator_name: str | None = None,
mechanical_reduction: float = 1.0,
hardware_interfaces: list[str] | None = None,
) -> Transmission:
"""Create a simple 1-to-1 transmission.
Args:
name: Transmission name
joint_name: Name of the joint
actuator_name: Name of the actuator (defaults to joint_name + "_motor")
mechanical_reduction: Gear ratio (default 1.0)
hardware_interfaces: Interface types (default [HW_IF_POSITION])
Returns:
Configured simple transmission
"""
if actuator_name is None:
actuator_name = f"{joint_name}_motor"
# Handle interfaces
actual_interfaces = hardware_interfaces or []
if not actual_interfaces:
actual_interfaces = [HW_IF_POSITION]
return cls(
name=name,
type=TransmissionType.SIMPLE.value,
joints=(
TransmissionJoint(
name=joint_name,
hardware_interfaces=actual_interfaces,
mechanical_reduction=mechanical_reduction,
),
),
actuators=(
TransmissionActuator(
name=actuator_name,
hardware_interfaces=actual_interfaces,
mechanical_reduction=1.0,
),
),
)
[docs]
@classmethod
def create_differential(
cls,
name: str,
joint1_name: str,
joint2_name: str,
actuator1_name: str | None = None,
actuator2_name: str | None = None,
mechanical_reduction: float = 1.0,
hardware_interfaces: list[str] | None = None,
) -> Transmission:
"""Create a differential transmission (2 actuators, 2 joints).
Args:
name: Transmission name
joint1_name: First joint name
joint2_name: Second joint name
actuator1_name: First actuator name (defaults to joint1_name + "_motor")
actuator2_name: Second actuator name (defaults to joint2_name + "_motor")
mechanical_reduction: Gear ratio (default 1.0)
hardware_interfaces: Interface types (default [HW_IF_POSITION])
Returns:
Configured differential transmission
"""
if actuator1_name is None:
actuator1_name = f"{joint1_name}_motor"
if actuator2_name is None:
actuator2_name = f"{joint2_name}_motor"
# Handle interfaces
actual_interfaces = hardware_interfaces or []
if not actual_interfaces:
actual_interfaces = [HW_IF_POSITION]
return cls(
name=name,
type=TransmissionType.DIFFERENTIAL.value,
joints=(
TransmissionJoint(
name=joint1_name,
hardware_interfaces=actual_interfaces,
mechanical_reduction=mechanical_reduction,
),
TransmissionJoint(
name=joint2_name,
hardware_interfaces=actual_interfaces,
mechanical_reduction=mechanical_reduction,
),
),
actuators=(
TransmissionActuator(
name=actuator1_name,
hardware_interfaces=actual_interfaces,
mechanical_reduction=1.0,
),
TransmissionActuator(
name=actuator2_name,
hardware_interfaces=actual_interfaces,
mechanical_reduction=1.0,
),
),
)