Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions dimos/control/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,38 @@ def task_invoke(

return getattr(task, method)(**kwargs)

# =========================================================================
# Gripper
# =========================================================================

@rpc
def set_gripper_position(self, hardware_id: str, position: float) -> bool:
"""Set gripper position on a specific hardware device.

Args:
hardware_id: ID of the hardware with the gripper
position: Gripper position in meters
"""
with self._hardware_lock:
hw = self._hardware.get(hardware_id)
if hw is None:
logger.warning(f"Hardware '{hardware_id}' not found for gripper command")
return False
return hw.adapter.write_gripper_position(position)

@rpc
def get_gripper_position(self, hardware_id: str) -> float | None:
"""Get gripper position from a specific hardware device.

Args:
hardware_id: ID of the hardware with the gripper
"""
with self._hardware_lock:
hw = self._hardware.get(hardware_id)
if hw is None:
return None
return hw.adapter.read_gripper_position()

# =========================================================================
# Lifecycle
# =========================================================================
Expand Down
5 changes: 5 additions & 0 deletions dimos/control/hardware_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ def __init__(
self._warned_unknown_joints: set[str] = set()
self._current_mode: ControlMode | None = None

@property
def adapter(self) -> ManipulatorAdapter:
"""The underlying hardware adapter."""
return self._adapter

@property
def hardware_id(self) -> HardwareId:
"""Unique ID for this hardware."""
Expand Down
3 changes: 2 additions & 1 deletion dimos/hardware/manipulators/xarm/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,9 @@ def write_gripper_position(self, position: float) -> bool:
if not self._arm:
return False

self._arm.set_gripper_enable(True)
pos_mm = position * M_TO_MM
code: int = self._arm.set_gripper_position(pos_mm)
code: int = self._arm.set_gripper_position(pos_mm, wait=True)
return code == 0

# =========================================================================
Expand Down
74 changes: 73 additions & 1 deletion dimos/manipulation/manipulation_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,9 @@ def get_robot_info(self, robot_name: RobotName | None = None) -> dict[str, objec

def _get_coordinator_client(self) -> RPCClient | None:
"""Get or create coordinator RPC client (lazy init)."""
if not any(c.coordinator_task_name for _, c, _ in self._robots.values()):
if not any(
c.coordinator_task_name or c.gripper_hardware_id for _, c, _ in self._robots.values()
):
return None
if self._coordinator_client is None:
from dimos.control.coordinator import ControlCoordinator
Expand Down Expand Up @@ -650,6 +652,76 @@ def remove_obstacle(self, obstacle_id: str) -> bool:
return False
return self._world_monitor.remove_obstacle(obstacle_id)

# =========================================================================
# Gripper RPC Methods
# =========================================================================

def _get_gripper_hardware_id(self, robot_name: RobotName | None = None) -> str | None:
"""Get gripper hardware ID for a robot."""
robot = self._get_robot(robot_name)
if robot is None:
return None
_, _, config, _ = robot
if not config.gripper_hardware_id:
logger.warning(f"No gripper_hardware_id configured for '{config.name}'")
return None
return config.gripper_hardware_id

@rpc
def set_gripper(self, position: float, robot_name: RobotName | None = None) -> bool:
"""Set gripper position in meters.

Args:
position: Gripper position in meters
robot_name: Robot to control (required if multiple robots configured)
"""
hw_id = self._get_gripper_hardware_id(robot_name)
if hw_id is None:
return False
client = self._get_coordinator_client()
if client is None:
logger.error("No coordinator client for gripper control")
return False
return bool(client.set_gripper_position(hw_id, position))

@rpc
def get_gripper(self, robot_name: RobotName | None = None) -> float | None:
"""Get gripper position in meters.

Args:
robot_name: Robot to query (required if multiple robots configured)
"""
hw_id = self._get_gripper_hardware_id(robot_name)
if hw_id is None:
return None
client = self._get_coordinator_client()
if client is None:
return None
result = client.get_gripper_position(hw_id)
return float(result) if result is not None else None

@rpc
def open_gripper(self, robot_name: RobotName | None = None) -> bool:
"""Open gripper fully (0.85m opening).

Args:
robot_name: Robot to control (required if multiple robots configured)
"""
return bool(self.set_gripper(0.85, robot_name))

@rpc
def close_gripper(self, robot_name: RobotName | None = None) -> bool:
"""Close gripper fully.

Args:
robot_name: Robot to control (required if multiple robots configured)
"""
return bool(self.set_gripper(0.0, robot_name))

# =========================================================================
# Lifecycle
# =========================================================================

@rpc
def stop(self) -> None:
"""Stop the manipulation module."""
Expand Down
48 changes: 48 additions & 0 deletions dimos/manipulation/planning/examples/manipulation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,43 @@ def remove(self, obstacle_id: str) -> bool:
"""Remove obstacle."""
return cast("bool", self._call("remove_obstacle", obstacle_id))

# =========================================================================
# Gripper Methods
# =========================================================================

def set_gripper(self, position: float, robot_name: str | None = None) -> bool:
"""Set gripper position in meters.

Args:
position: Gripper position in meters
robot_name: Robot to control (required if multiple robots configured)
"""
return cast("bool", self._call("set_gripper", position, robot_name=robot_name))

def get_gripper(self, robot_name: str | None = None) -> float | None:
"""Get gripper position in meters.

Args:
robot_name: Robot to query (required if multiple robots configured)
"""
return cast("float | None", self._call("get_gripper", robot_name=robot_name))

def open_gripper(self, robot_name: str | None = None) -> bool:
"""Open gripper fully.

Args:
robot_name: Robot to control (required if multiple robots configured)
"""
return cast("bool", self._call("open_gripper", robot_name=robot_name))

def close_gripper(self, robot_name: str | None = None) -> bool:
"""Close gripper fully.

Args:
robot_name: Robot to control (required if multiple robots configured)
"""
return cast("bool", self._call("close_gripper", robot_name=robot_name))

# =========================================================================
# Utility Methods
# =========================================================================
Expand Down Expand Up @@ -289,6 +326,11 @@ def main() -> None:
"sphere": c.sphere,
"cylinder": c.cylinder,
"remove": c.remove,
# Gripper methods
"set_gripper": c.set_gripper,
"get_gripper": c.get_gripper,
"open_gripper": c.open_gripper,
"close_gripper": c.close_gripper,
# Utility methods
"collision": c.collision,
"reset": c.reset,
Expand Down Expand Up @@ -321,6 +363,12 @@ def main() -> None:
cylinder("name", x, y, z, radius, length) # Add cylinder
remove("obstacle_id") # Remove obstacle

Gripper:
open_gripper() # Open gripper fully
close_gripper() # Close gripper fully
set_gripper(0.05) # Set gripper position (meters)
get_gripper() # Get gripper position (meters)

Utility:
collision([0.1, ...]) # Check if config is collision-free
reset() # Reset to IDLE state
Expand Down
1 change: 1 addition & 0 deletions dimos/manipulation/planning/spec/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class RobotModelConfig:
# Coordinator integration
joint_name_mapping: dict[str, str] = field(default_factory=dict)
coordinator_task_name: str | None = None
gripper_hardware_id: str | None = None

def get_urdf_joint_name(self, coordinator_name: str) -> str:
"""Translate coordinator joint name to URDF joint name."""
Expand Down
Loading