Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fea963e
initial commit
ruthwikdasyam Feb 9, 2026
cf2021d
Feat: pinnochio_ik to manipulation/planning/kinematics
ruthwikdasyam Feb 9, 2026
018c5ee
using pinnochio class
ruthwikdasyam Feb 10, 2026
4a3258e
Feat: taking in QuestButtons stream
ruthwikdasyam Feb 10, 2026
a70f343
Fix: pre-commit fixes
ruthwikdasyam Feb 10, 2026
81f0f96
Merge branch 'dev' into ruthwik_arm_teleop
ruthwikdasyam Feb 10, 2026
299cb63
LFS: Xarm6 URDF created from xacro
ruthwikdasyam Feb 11, 2026
69a77bd
Feat: blueprints added for teleop xarm, piper, dual tasks
ruthwikdasyam Feb 11, 2026
62b7dbe
Feat: teleop_ik task upgrades
ruthwikdasyam Feb 11, 2026
e9f20ca
Feat: arm teleop support
ruthwikdasyam Feb 11, 2026
7f0133e
Fix: pre-commit errors
ruthwikdasyam Feb 11, 2026
848311f
Fix: changed xarm6 urdf path in lfs
ruthwikdasyam Feb 11, 2026
a4a3217
cleanup
ruthwikdasyam Feb 12, 2026
4d55ec0
Merge branch 'dev' into ruthwik_arm_teleop
ruthwikdasyam Feb 12, 2026
1c08c76
Fix: fix redundant code
ruthwikdasyam Feb 12, 2026
a84177d
Fix: _hasattr check added
ruthwikdasyam Feb 12, 2026
1216332
Fix: improper lock errors + remove fallback
ruthwikdasyam Feb 12, 2026
e874e4d
Fix: mypy issues - initial_pos check
ruthwikdasyam Feb 12, 2026
1c43535
add LfsPath helper
jeff-hykin Feb 13, 2026
69e225c
Fix: removing ignore + config fix
ruthwikdasyam Feb 13, 2026
b4e1a27
Fix: pose_to_se3 standalone method + remove redundant dict_pose conv…
ruthwikdasyam Feb 13, 2026
0df2ea0
method reorder + add warning
ruthwikdasyam Feb 13, 2026
0e7872c
Feat: Added BaseControlTask + removal of hasattr for methodchecking
ruthwikdasyam Feb 13, 2026
1842857
Fix: pre-commit fixes
ruthwikdasyam Feb 13, 2026
84a2efb
Fix: remove .transport checks in coordinator subscriptions
ruthwikdasyam Feb 13, 2026
77cb5e9
Comments restructure
ruthwikdasyam Feb 14, 2026
2f9b0c1
fix: LfsPath resolution
ruthwikdasyam Feb 14, 2026
fee4c6a
fix: replace python-version-specific safe attributes with try/except …
ruthwikdasyam Feb 14, 2026
73837f8
Merge branch 'dev' into ruthwik_arm_teleop
ruthwikdasyam Feb 14, 2026
d1c8d0e
Fix: merge conflicts
ruthwikdasyam Feb 14, 2026
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
4 changes: 2 additions & 2 deletions data/.lfs/xarm_description.tar.gz
Git LFS file not shown
150 changes: 140 additions & 10 deletions dimos/control/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
from dimos.core.transport import LCMTransport
from dimos.msgs.geometry_msgs import PoseStamped
from dimos.msgs.sensor_msgs import JointState
from dimos.teleop.quest.quest_types import QuestButtons
from dimos.utils.data import LfsPath

_PIPER_MODEL_PATH = LfsPath("piper_description/mujoco_model/piper_no_gripper_description.xml")
_XARM6_MODEL_PATH = LfsPath("xarm_description/urdf/xarm6/xarm6.urdf")

# =============================================================================
# Single Arm Blueprints
Expand Down Expand Up @@ -391,14 +396,6 @@
# =============================================================================


def _get_piper_model_path() -> str:
"""Get path to Piper MJCF model for IK solver."""
from dimos.utils.data import get_data

piper_path = get_data("piper_description")
return str(piper_path / "mujoco_model" / "piper_no_gripper_description.xml")


# Mock 6-DOF arm with CartesianIK
coordinator_cartesian_ik_mock = control_coordinator(
tick_rate=100.0,
Expand All @@ -418,7 +415,7 @@ def _get_piper_model_path() -> str:
type="cartesian_ik",
joint_names=[f"arm_joint{i + 1}" for i in range(6)],
priority=10,
model_path=_get_piper_model_path(),
model_path=_PIPER_MODEL_PATH,
ee_joint_id=6,
),
],
Expand Down Expand Up @@ -452,8 +449,137 @@ def _get_piper_model_path() -> str:
type="cartesian_ik",
joint_names=[f"arm_joint{i + 1}" for i in range(6)],
priority=10,
model_path=_get_piper_model_path(),
model_path=_PIPER_MODEL_PATH,
ee_joint_id=6,
),
],
).transports(
{
("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState),
("cartesian_command", PoseStamped): LCMTransport(
"/coordinator/cartesian_command", PoseStamped
),
}
)


# =============================================================================
# Teleop IK Blueprints (VR teleoperation with internal Pinocchio IK)
# =============================================================================

# Single XArm6 with TeleopIK
coordinator_teleop_xarm6 = control_coordinator(
tick_rate=100.0,
publish_joint_state=True,
joint_state_frame_id="coordinator",
hardware=[
HardwareComponent(
hardware_id="arm",
hardware_type=HardwareType.MANIPULATOR,
joints=make_joints("arm", 6),
adapter_type="xarm",
address="192.168.1.210",
auto_enable=True,
),
],
tasks=[
TaskConfig(
name="teleop_xarm",
type="teleop_ik",
joint_names=[f"arm_joint{i + 1}" for i in range(6)],
priority=10,
model_path=_XARM6_MODEL_PATH,
ee_joint_id=6,
hand="right",
),
],
).transports(
{
("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState),
("cartesian_command", PoseStamped): LCMTransport(
"/coordinator/cartesian_command", PoseStamped
),
("buttons", QuestButtons): LCMTransport("/teleop/buttons", QuestButtons),
}
)

# Single Piper with TeleopIK
coordinator_teleop_piper = control_coordinator(
tick_rate=100.0,
publish_joint_state=True,
joint_state_frame_id="coordinator",
hardware=[
HardwareComponent(
hardware_id="arm",
hardware_type=HardwareType.MANIPULATOR,
joints=make_joints("arm", 6),
adapter_type="piper",
address="can0",
auto_enable=True,
),
],
tasks=[
TaskConfig(
name="teleop_piper",
type="teleop_ik",
joint_names=[f"arm_joint{i + 1}" for i in range(6)],
priority=10,
model_path=_PIPER_MODEL_PATH,
ee_joint_id=6,
hand="left",
),
],
).transports(
{
("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState),
("cartesian_command", PoseStamped): LCMTransport(
"/coordinator/cartesian_command", PoseStamped
),
("buttons", QuestButtons): LCMTransport("/teleop/buttons", QuestButtons),
}
)

# Dual arm teleop: XArm6 + Piper with TeleopIK
coordinator_teleop_dual = control_coordinator(
tick_rate=100.0,
publish_joint_state=True,
joint_state_frame_id="coordinator",
hardware=[
HardwareComponent(
hardware_id="xarm_arm",
hardware_type=HardwareType.MANIPULATOR,
joints=make_joints("xarm_arm", 6),
adapter_type="xarm",
address="192.168.1.210",
auto_enable=True,
),
HardwareComponent(
hardware_id="piper_arm",
hardware_type=HardwareType.MANIPULATOR,
joints=make_joints("piper_arm", 6),
adapter_type="piper",
address="can0",
auto_enable=True,
),
],
tasks=[
TaskConfig(
name="teleop_xarm",
type="teleop_ik",
joint_names=[f"xarm_arm_joint{i + 1}" for i in range(6)],
priority=10,
model_path=_XARM6_MODEL_PATH,
ee_joint_id=6,
hand="left",
),
TaskConfig(
name="teleop_piper",
type="teleop_ik",
joint_names=[f"piper_arm_joint{i + 1}" for i in range(6)],
priority=10,
model_path=_PIPER_MODEL_PATH,
ee_joint_id=6,
hand="right",
),
],
).transports(
Expand All @@ -462,6 +588,7 @@ def _get_piper_model_path() -> str:
("cartesian_command", PoseStamped): LCMTransport(
"/coordinator/cartesian_command", PoseStamped
),
("buttons", QuestButtons): LCMTransport("/teleop/buttons", QuestButtons),
}
)

Expand Down Expand Up @@ -500,6 +627,9 @@ def _get_piper_model_path() -> str:
"coordinator_mock",
"coordinator_piper",
"coordinator_piper_xarm",
# Teleop IK
"coordinator_teleop_dual",
"coordinator_teleop_piper",
"coordinator_teleop_xarm6",
"coordinator_velocity_xarm6",
"coordinator_xarm6",
Expand Down
104 changes: 66 additions & 38 deletions dimos/control/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
from dimos.msgs.sensor_msgs import (
JointState, # noqa: TC001 - needed at runtime for Out[JointState]
)
from dimos.teleop.quest.quest_types import QuestButtons # noqa: TC001 - needed for teleop buttons
from dimos.utils.logging_config import setup_logger

if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path

from dimos.hardware.manipulators.spec import ManipulatorAdapter

Expand All @@ -65,20 +67,21 @@ class TaskConfig:

Attributes:
name: Task name (e.g., "traj_arm")
type: Task type ("trajectory", "servo", "velocity", "cartesian_ik")
type: Task type ("trajectory", "servo", "velocity", "cartesian_ik", "teleop_ik")
joint_names: List of joint names this task controls
priority: Task priority (higher wins arbitration)
model_path: Path to URDF/MJCF for IK solver (cartesian_ik only)
ee_joint_id: End-effector joint ID in model (cartesian_ik only)
model_path: Path to URDF/MJCF for IK solver (cartesian_ik/teleop_ik only)
ee_joint_id: End-effector joint ID in model (cartesian_ik/teleop_ik only)
"""

name: str
type: str = "trajectory"
joint_names: list[str] = field(default_factory=lambda: [])
priority: int = 10
# Cartesian IK specific
model_path: str | None = None
# Cartesian IK / Teleop IK specific
model_path: str | Path | None = None
ee_joint_id: int = 6
hand: str = "" # teleop_ik only: "left" or "right" controller


@dataclass
Expand Down Expand Up @@ -145,6 +148,9 @@ class ControlCoordinator(Module[ControlCoordinatorConfig]):
# Uses frame_id as task name for routing
cartesian_command: In[PoseStamped]

# Input: Teleop buttons for engage/disengage signaling
buttons: In[QuestButtons]
Copy link
Member

@jeff-hykin jeff-hykin Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quest = MetaQuest buttons right? I think this needs to be generic. What would it look like if we were to add PicoButtons?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buttons stream name is generic, but the message type is QuestButtons since that's the only controller we support right now. Once we add a second device (Pico or VisionPro), the right generic interface will be clearer. abstracting before, without a second device/case usually leads to the wrong API. Keeping as-is for now.


config: ControlCoordinatorConfig
default_config = ControlCoordinatorConfig

Expand All @@ -168,6 +174,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
# Subscription handles for streaming commands
self._joint_command_unsub: Callable[[], None] | None = None
self._cartesian_command_unsub: Callable[[], None] | None = None
self._buttons_unsub: Callable[[], None] | None = None

logger.info(f"ControlCoordinator initialized at {self.config.tick_rate}Hz")

Expand Down Expand Up @@ -276,6 +283,23 @@ def _create_task_from_config(self, cfg: TaskConfig) -> ControlTask:
),
)

elif task_type == "teleop_ik":
from dimos.control.tasks.teleop_task import TeleopIKTask, TeleopIKTaskConfig

if cfg.model_path is None:
raise ValueError(f"TeleopIKTask '{cfg.name}' requires model_path in TaskConfig")

return TeleopIKTask(
cfg.name,
TeleopIKTaskConfig(
joint_names=cfg.joint_names,
model_path=cfg.model_path,
ee_joint_id=cfg.ee_joint_id,
priority=cfg.priority,
hand=cfg.hand,
),
)

else:
raise ValueError(f"Unknown task type: {task_type}")

Expand Down Expand Up @@ -437,14 +461,14 @@ def _on_joint_command(self, msg: JointState) -> None:
continue

# Route to servo tasks (position control)
if hasattr(task, "set_target_by_name") and msg.position:
if msg.position:
positions_by_name = dict(zip(msg.name, msg.position, strict=False))
task.set_target_by_name(positions_by_name, t_now) # type: ignore[attr-defined]
task.set_target_by_name(positions_by_name, t_now)

# Route to velocity tasks (velocity control)
elif hasattr(task, "set_velocities_by_name") and msg.velocity:
elif msg.velocity:
velocities_by_name = dict(zip(msg.name, msg.velocity, strict=False))
task.set_velocities_by_name(velocities_by_name, t_now) # type: ignore[attr-defined]
task.set_velocities_by_name(velocities_by_name, t_now)

def _on_cartesian_command(self, msg: PoseStamped) -> None:
"""Route incoming PoseStamped to CartesianIKTask by task name.
Expand All @@ -464,11 +488,13 @@ def _on_cartesian_command(self, msg: PoseStamped) -> None:
logger.warning(f"Cartesian command for unknown task: {task_name}")
return

# Route to CartesianIKTask
if hasattr(task, "set_target_pose"):
task.set_target_pose(msg, t_now) # type: ignore[attr-defined]
else:
logger.warning(f"Task {task_name} does not support set_target_pose")
task.on_cartesian_command(msg, t_now)

def _on_buttons(self, msg: QuestButtons) -> None:
"""Forward button state to all tasks."""
with self._task_lock:
for task in self._tasks.values():
task.on_buttons(msg)

@rpc
def task_invoke(
Expand Down Expand Up @@ -561,35 +587,34 @@ def start(self) -> None:
streaming_types = ("servo", "velocity")
has_streaming = any(t.type in streaming_types for t in self.config.tasks)
if has_streaming:
# Only subscribe if transport is configured
try:
if self.joint_command.transport:
self._joint_command_unsub = self.joint_command.subscribe(self._on_joint_command)
logger.info("Subscribed to joint_command for streaming tasks")
else:
logger.warning(
"Streaming tasks configured but no transport set for joint_command. "
"Use task_invoke RPC or set transport via blueprint."
)
except Exception as e:
logger.warning(f"Could not subscribe to joint_command: {e}")
self._joint_command_unsub = self.joint_command.subscribe(self._on_joint_command)
logger.info("Subscribed to joint_command for streaming tasks")
except Exception:
logger.warning(
"Streaming tasks configured but could not subscribe to joint_command. "
"Use task_invoke RPC or set transport via blueprint."
)

# Subscribe to cartesian commands if any cartesian_ik tasks configured
has_cartesian_ik = any(t.type == "cartesian_ik" for t in self.config.tasks)
has_cartesian_ik = any(t.type in ("cartesian_ik", "teleop_ik") for t in self.config.tasks)
if has_cartesian_ik:
try:
if self.cartesian_command.transport:
self._cartesian_command_unsub = self.cartesian_command.subscribe(
self._on_cartesian_command
)
logger.info("Subscribed to cartesian_command for CartesianIK tasks")
else:
logger.warning(
"CartesianIK tasks configured but no transport set for cartesian_command. "
"Use task_invoke RPC or set transport via blueprint."
)
except Exception as e:
logger.warning(f"Could not subscribe to cartesian_command: {e}")
self._cartesian_command_unsub = self.cartesian_command.subscribe(
self._on_cartesian_command
)
logger.info("Subscribed to cartesian_command for CartesianIK/TeleopIK tasks")
except Exception:
logger.warning(
"CartesianIK/TeleopIK tasks configured but could not subscribe to cartesian_command. "
"Use task_invoke RPC or set transport via blueprint."
)

# Subscribe to buttons if any teleop_ik tasks configured (engage/disengage)
has_teleop_ik = any(t.type == "teleop_ik" for t in self.config.tasks)
if has_teleop_ik:
self._buttons_unsub = self.buttons.subscribe(self._on_buttons)
logger.info("Subscribed to buttons for engage/disengage")

logger.info(f"ControlCoordinator started at {self.config.tick_rate}Hz")

Expand All @@ -605,6 +630,9 @@ def stop(self) -> None:
if self._cartesian_command_unsub:
self._cartesian_command_unsub()
self._cartesian_command_unsub = None
if self._buttons_unsub:
self._buttons_unsub()
self._buttons_unsub = None

if self._tick_loop:
self._tick_loop.stop()
Expand Down
Loading
Loading