Skip to content

Commit 2f5d17a

Browse files
feat: integrate gripper into coordinator tick loop (#1371)
* Feat: modified buttons to publish absolute trigger values * Fix: control loop start must be in start * feat: packing analog triggers iwith buttons in UInt32 * for xarm7 * Feat: added gripper as hardware_component * Feat: gripper trigger decode * Fix: pre-commit fixes * comments update * fix: bit conversion amth + comments * fix: comments and errors * fix comments + pre-commit errors * fix: blueprint + mypy tests * CI code cleanup * Fix: no gripper as component, adding with joints * feat: add literal for hand str * fix: arm and gripper joint params * CI code cleanup * Misc: docstrings
1 parent 033f6d5 commit 2f5d17a

12 files changed

Lines changed: 207 additions & 55 deletions

File tree

dimos/control/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"HardwareType",
6161
"JointName",
6262
"JointState",
63+
"make_gripper_joints",
6364
"make_joints",
6465
],
6566
"coordinator": [

dimos/control/blueprints.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from dimos.control.components import (
3434
HardwareComponent,
3535
HardwareType,
36+
make_gripper_joints,
3637
make_joints,
3738
make_twist_base_joints,
3839
)
@@ -45,6 +46,7 @@
4546

4647
_PIPER_MODEL_PATH = LfsPath("piper_description/mujoco_model/piper_no_gripper_description.xml")
4748
_XARM6_MODEL_PATH = LfsPath("xarm_description/urdf/xarm6/xarm6.urdf")
49+
_XARM7_MODEL_PATH = LfsPath("xarm_description/urdf/xarm7/xarm7.urdf")
4850

4951

5052
# =============================================================================
@@ -473,30 +475,34 @@
473475
# Teleop IK Blueprints (VR teleoperation with internal Pinocchio IK)
474476
# =============================================================================
475477

476-
# Single XArm6 with TeleopIK
477-
coordinator_teleop_xarm6 = control_coordinator(
478+
# Single XArm7 with TeleopIK
479+
coordinator_teleop_xarm7 = control_coordinator(
478480
tick_rate=100.0,
479481
publish_joint_state=True,
480482
joint_state_frame_id="coordinator",
481483
hardware=[
482484
HardwareComponent(
483485
hardware_id="arm",
484486
hardware_type=HardwareType.MANIPULATOR,
485-
joints=make_joints("arm", 6),
487+
joints=make_joints("arm", 7),
486488
adapter_type="xarm",
487-
address="192.168.1.210",
489+
address="192.168.2.235",
488490
auto_enable=True,
491+
gripper_joints=make_gripper_joints("arm"),
489492
),
490493
],
491494
tasks=[
492495
TaskConfig(
493496
name="teleop_xarm",
494497
type="teleop_ik",
495-
joint_names=[f"arm_joint{i + 1}" for i in range(6)],
498+
joint_names=[f"arm_joint{i + 1}" for i in range(7)],
496499
priority=10,
497-
model_path=_XARM6_MODEL_PATH,
498-
ee_joint_id=6,
500+
model_path=_XARM7_MODEL_PATH,
501+
ee_joint_id=7,
499502
hand="right",
503+
gripper_joint=make_gripper_joints("arm")[0],
504+
gripper_open_pos=0.85, # xArm gripper range
505+
gripper_closed_pos=0.0,
500506
),
501507
],
502508
).transports(
@@ -715,6 +721,7 @@
715721
"coordinator_teleop_dual",
716722
"coordinator_teleop_piper",
717723
"coordinator_teleop_xarm6",
724+
"coordinator_teleop_xarm7",
718725
"coordinator_velocity_xarm6",
719726
"coordinator_xarm6",
720727
"coordinator_xarm7",

dimos/control/components.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
class HardwareType(Enum):
2626
MANIPULATOR = "manipulator"
2727
BASE = "base"
28-
GRIPPER = "gripper"
2928

3029

3130
@dataclass(frozen=True)
@@ -43,11 +42,12 @@ class HardwareComponent:
4342
4443
Attributes:
4544
hardware_id: Unique identifier, also used as joint name prefix
46-
hardware_type: Type of hardware (MANIPULATOR, BASE, GRIPPER)
45+
hardware_type: Type of hardware (MANIPULATOR, BASE)
4746
joints: List of joint names (e.g., ["arm_joint1", "arm_joint2", ...])
4847
adapter_type: Adapter type ("mock", "xarm", "piper")
4948
address: Connection address - IP for TCP, port for CAN
5049
auto_enable: Whether to auto-enable servos
50+
gripper_joints: Joints that use adapter gripper methods (separate from joints).
5151
"""
5252

5353
hardware_id: HardwareId
@@ -56,6 +56,24 @@ class HardwareComponent:
5656
adapter_type: str = "mock"
5757
address: str | None = None
5858
auto_enable: bool = True
59+
gripper_joints: list[JointName] = field(default_factory=list)
60+
61+
@property
62+
def all_joints(self) -> list[JointName]:
63+
"""All joints: arm joints + gripper joints."""
64+
return self.joints + self.gripper_joints
65+
66+
67+
def make_gripper_joints(hardware_id: HardwareId) -> list[JointName]:
68+
"""Create gripper joint names for a hardware device.
69+
70+
Args:
71+
hardware_id: The hardware identifier (e.g., "arm")
72+
73+
Returns:
74+
List of joint names like ["arm_gripper"]
75+
"""
76+
return [f"{hardware_id}_gripper"]
5977

6078

6179
def make_joints(hardware_id: HardwareId, dof: int) -> list[JointName]:
@@ -112,6 +130,7 @@ def make_twist_base_joints(
112130
"JointName",
113131
"JointState",
114132
"TaskName",
133+
"make_gripper_joints",
115134
"make_joints",
116135
"make_twist_base_joints",
117136
]

dimos/control/coordinator.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from pathlib import Path
3030
import threading
3131
import time
32-
from typing import TYPE_CHECKING, Any
32+
from typing import TYPE_CHECKING, Any, Literal
3333

3434
from dimos.control.components import (
3535
TWIST_SUFFIX_MAP,
@@ -84,6 +84,10 @@ class TaskConfig:
8484
priority: Task priority (higher wins arbitration)
8585
model_path: Path to URDF/MJCF for IK solver (cartesian_ik/teleop_ik only)
8686
ee_joint_id: End-effector joint ID in model (cartesian_ik/teleop_ik only)
87+
hand: "left" or "right" controller hand (teleop_ik only)
88+
gripper_joint: Joint name for gripper virtual joint
89+
gripper_open_pos: Gripper position at trigger 0.0
90+
gripper_closed_pos: Gripper position at trigger 1.0
8791
"""
8892

8993
name: str
@@ -93,7 +97,11 @@ class TaskConfig:
9397
# Cartesian IK / Teleop IK specific
9498
model_path: str | Path | None = None
9599
ee_joint_id: int = 6
96-
hand: str = "" # teleop_ik only: "left" or "right" controller
100+
hand: Literal["left", "right"] | None = None # teleop_ik only
101+
# Teleop IK gripper specific
102+
gripper_joint: str | None = None
103+
gripper_open_pos: float = 0.0
104+
gripper_closed_pos: float = 0.0
97105

98106

99107
@dataclass
@@ -327,6 +335,9 @@ def _create_task_from_config(self, cfg: TaskConfig) -> ControlTask:
327335
ee_joint_id=cfg.ee_joint_id,
328336
priority=cfg.priority,
329337
hand=cfg.hand,
338+
gripper_joint=cfg.gripper_joint,
339+
gripper_open_pos=cfg.gripper_open_pos,
340+
gripper_closed_pos=cfg.gripper_closed_pos,
330341
),
331342
)
332343

@@ -345,6 +356,7 @@ def add_hardware(
345356
) -> bool:
346357
"""Register a hardware adapter with the coordinator."""
347358
is_base = component.hardware_type == HardwareType.BASE
359+
348360
if is_base != isinstance(adapter, TwistBaseAdapter):
349361
raise TypeError(
350362
f"Hardware type / adapter mismatch for '{component.hardware_id}': "

dimos/control/hardware_interface.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def __init__(
6565

6666
self._adapter = adapter
6767
self._component = component
68-
self._joint_names = component.joints
68+
self._arm_joint_names: list[JointName] = list(component.joints)
69+
self._gripper_joints: list[JointName] = list(component.gripper_joints)
70+
self._joint_names: list[JointName] = component.all_joints
6971

7072
# Track last commanded values for hold-last behavior
7173
self._last_commanded: dict[str, float] = {}
@@ -114,15 +116,27 @@ def read_state(self) -> dict[JointName, JointState]:
114116
velocities = self._adapter.read_joint_velocities()
115117
efforts = self._adapter.read_joint_efforts()
116118

117-
return {
119+
result: dict[JointName, JointState] = {
118120
name: JointState(
119121
position=positions[i],
120122
velocity=velocities[i],
121123
effort=efforts[i],
122124
)
123-
for i, name in enumerate(self._joint_names)
125+
for i, name in enumerate(self._arm_joint_names)
124126
}
125127

128+
# Append gripper joint(s) via adapter gripper method
129+
if self._gripper_joints:
130+
gripper_pos = self._adapter.read_gripper_position()
131+
for gj in self._gripper_joints:
132+
result[gj] = JointState(
133+
position=gripper_pos if gripper_pos is not None else 0.0,
134+
velocity=0.0,
135+
effort=0.0,
136+
)
137+
138+
return result
139+
126140
def write_command(self, commands: dict[str, float], mode: ControlMode) -> bool:
127141
"""Write commands - allows partial joint sets, holds last for missing.
128142
@@ -153,8 +167,8 @@ def write_command(self, commands: dict[str, float], mode: ControlMode) -> bool:
153167
)
154168
self._warned_unknown_joints.add(joint_name)
155169

156-
# Build ordered list for adapter
157-
ordered = self._build_ordered_command()
170+
# Build ordered list for arm joints only
171+
arm_ordered = [self._last_commanded[name] for name in self._arm_joint_names]
158172

159173
# Switch control mode if needed
160174
if mode != self._current_mode:
@@ -163,25 +177,43 @@ def write_command(self, commands: dict[str, float], mode: ControlMode) -> bool:
163177
return False
164178
self._current_mode = mode
165179

166-
# Send to adapter
180+
# Send arm joints to adapter
181+
arm_ok: bool
167182
match mode:
168183
case ControlMode.POSITION | ControlMode.SERVO_POSITION:
169-
return self._adapter.write_joint_positions(ordered)
184+
arm_ok = self._adapter.write_joint_positions(arm_ordered)
170185
case ControlMode.VELOCITY:
171-
return self._adapter.write_joint_velocities(ordered)
186+
arm_ok = self._adapter.write_joint_velocities(arm_ordered)
172187
case ControlMode.TORQUE:
173188
logger.warning(f"Hardware {self.hardware_id} does not support torque mode")
174-
return False
189+
arm_ok = False
175190
case _:
176-
return False
191+
arm_ok = False
192+
193+
# Send gripper joints via adapter gripper method
194+
gripper_ok = True
195+
for gj in self._gripper_joints:
196+
if gj in self._last_commanded:
197+
gripper_ok = (
198+
self._adapter.write_gripper_position(self._last_commanded[gj]) and gripper_ok
199+
)
200+
201+
return arm_ok and gripper_ok
177202

178203
def _initialize_last_commanded(self) -> None:
179204
"""Initialize last_commanded with current hardware positions."""
180205
for _ in range(10):
181206
try:
182207
current = self._adapter.read_joint_positions()
183-
for i, name in enumerate(self._joint_names):
208+
for i, name in enumerate(self._arm_joint_names):
184209
self._last_commanded[name] = current[i]
210+
211+
# Initialize gripper joint(s) from adapter
212+
if self._gripper_joints:
213+
gripper_pos = self._adapter.read_gripper_position()
214+
for gj in self._gripper_joints:
215+
self._last_commanded[gj] = gripper_pos if gripper_pos is not None else 0.0
216+
185217
self._initialized = True
186218
return
187219
except Exception:

0 commit comments

Comments
 (0)