Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ba4edaa
Initial Commit
ruthwikdasyam Jan 27, 2026
4cea4e9
CI code cleanup
ruthwikdasyam Jan 27, 2026
877e1e8
Feat: adding unit32
ruthwikdasyam Feb 6, 2026
bfe72d5
Feat: webXR and deno bridge
ruthwikdasyam Feb 6, 2026
179e809
Introducing base teleop protocol
ruthwikdasyam Feb 6, 2026
0824157
Feat: quest module and its subclasses
ruthwikdasyam Feb 6, 2026
5844c89
Feat: nice API for data from quest controllers
ruthwikdasyam Feb 6, 2026
737a1b1
Feat: Quest utils - visualization and transforms
ruthwikdasyam Feb 6, 2026
584dfbc
Docs: Readme added
ruthwikdasyam Feb 6, 2026
2cd8b8a
Feat: pose sub method
ruthwikdasyam Feb 6, 2026
4e2e3da
Misc: init updates
ruthwikdasyam Feb 6, 2026
9538e28
updating teleop blueprints
ruthwikdasyam Feb 6, 2026
2f3fabd
Feat: subclass for arm teleop
ruthwikdasyam Feb 6, 2026
19d4e0c
Fix: Bugs and cleanup
ruthwikdasyam Feb 6, 2026
8bc29f3
Misc: readme updates
ruthwikdasyam Feb 6, 2026
a7fcc50
Fix: pre-commit errors
ruthwikdasyam Feb 6, 2026
520a87c
Merge origin/dev into ruthwik_teleop
ruthwikdasyam Feb 6, 2026
9e46122
Feat: added locks, engage feat changes
ruthwikdasyam Feb 7, 2026
db8a4df
Misc: renaming methods and docstrings
ruthwikdasyam Feb 7, 2026
e2cb985
ignore teleop server certs
ruthwikdasyam Feb 7, 2026
13fffe3
Feat: trigger and grip sent through axes to preserve floats
ruthwikdasyam Feb 7, 2026
7c61378
Fix: disposable wrapper
ruthwikdasyam Feb 7, 2026
459cfad
Fix: deleted file
ruthwikdasyam Feb 7, 2026
1b0ecab
Merge branch 'dev' into ruthwik_teleop
ruthwikdasyam Feb 7, 2026
a16e712
Misc: docstrings, comments and mypy fixes
ruthwikdasyam Feb 8, 2026
3401d11
Misc: readme changes + added Constants
ruthwikdasyam Feb 8, 2026
e94574e
Feat: lock in control loop - monitor-style locking
ruthwikdasyam Feb 8, 2026
12f036f
Fix: resolve re-entrant lock in control loop and LCM init order
ruthwikdasyam Feb 8, 2026
92147a6
Fix: logging malperformed joy msgs
ruthwikdasyam Feb 8, 2026
84315c4
Fix: stream logging
ruthwikdasyam Feb 8, 2026
1ae2d7e
Fix: teleop certs path
ruthwikdasyam Feb 8, 2026
eac7ed1
Fix: server run cmd
ruthwikdasyam Feb 8, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ yolo11n.pt

*mobileclip*
/results

dimos/assets/teleop_certs/
Copy link
Contributor

Choose a reason for hiding this comment

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

dimos/ is only for code. Should this have been assets/teleop_certs/?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed this
server path + .gitignore are updated to /assets/teleop_certs/

13 changes: 13 additions & 0 deletions dimos/msgs/geometry_msgs/Pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,19 @@ def __add__(self, other: Pose | PoseConvertable | LCMTransform | Transform) -> P

return Pose(new_position, new_orientation)

def __sub__(self, other: Pose) -> Pose:
"""Compute the delta pose: self - other.

For position: simple subtraction.
For orientation: delta_quat = self.orientation * inverse(other.orientation)

Returns:
A new Pose representing the delta transformation
"""
delta_position = self.position - other.position
delta_orientation = self.orientation * other.orientation.inverse()
return Pose(delta_position, delta_orientation)

@classmethod
def from_ros_msg(cls, ros_msg: ROSPose) -> Pose:
"""Create a Pose from a ROS geometry_msgs/Pose message.
Expand Down
30 changes: 30 additions & 0 deletions dimos/msgs/std_msgs/UInt32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# Copyright 2025-2026 Dimensional Inc.
#
# 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.

"""UInt32 message type."""

from typing import ClassVar

from dimos_lcm.std_msgs import UInt32 as LCMUInt32


class UInt32(LCMUInt32): # type: ignore[misc]
"""ROS-compatible UInt32 message."""

msg_name: ClassVar[str] = "std_msgs.UInt32"

def __init__(self, data: int = 0) -> None:
"""Initialize UInt32 with data value."""
self.data = data
3 changes: 2 additions & 1 deletion dimos/msgs/std_msgs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
from .Header import Header
from .Int8 import Int8
from .Int32 import Int32
from .UInt32 import UInt32

__all__ = ["Bool", "Header", "Int8", "Int32"]
__all__ = ["Bool", "Header", "Int8", "Int32", "UInt32"]
6 changes: 6 additions & 0 deletions dimos/robot/all_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# Run `pytest dimos/robot/test_all_blueprints_generation.py` to regenerate.

all_blueprints = {
"arm-teleop": "dimos.teleop.blueprints:arm_teleop",
"arm-teleop-visualizing": "dimos.teleop.blueprints:arm_teleop_visualizing",
"coordinator-basic": "dimos.control.blueprints:coordinator_basic",
"coordinator-cartesian-ik-mock": "dimos.control.blueprints:coordinator_cartesian_ik_mock",
"coordinator-cartesian-ik-piper": "dimos.control.blueprints:coordinator_cartesian_ik_piper",
Expand Down Expand Up @@ -65,6 +67,7 @@


all_modules = {
"arm_teleop_module": "dimos.teleop.quest.quest_extensions",
"camera_module": "dimos.hardware.sensors.camera.module",
"cartesian_motion_controller": "dimos.manipulation.control.servo_control.cartesian_motion_controller",
"control_coordinator": "dimos.control.coordinator",
Expand Down Expand Up @@ -94,15 +97,18 @@
"osm_skill": "dimos.agents.skills.osm",
"person_follow_skill": "dimos.agents.skills.person_follow",
"person_tracker_module": "dimos.perception.detection.person_tracker",
"quest_teleop_module": "dimos.teleop.quest.quest_teleop_module",
"realsense_camera": "dimos.hardware.sensors.camera.realsense.camera",
"replanning_a_star_planner": "dimos.navigation.replanning_a_star.module",
"rerun_scene_wiring": "dimos.dashboard.rerun_scene_wiring",
"ros_nav": "dimos.navigation.rosnav",
"spatial_memory": "dimos.perception.spatial_perception",
"speak_skill": "dimos.agents.skills.speak_skill",
"temporal_memory": "dimos.perception.experimental.temporal_memory.temporal_memory",
"twist_teleop_module": "dimos.teleop.quest.quest_extensions",
"unitree_skills": "dimos.robot.unitree_webrtc.unitree_skill_container",
"utilization": "dimos.utils.monitoring",
"visualizing_teleop_module": "dimos.teleop.quest.quest_extensions",
"vlm_agent": "dimos.agents.vlm_agent",
"vlm_stream_tester": "dimos.agents.vlm_stream_tester",
"voxel_mapper": "dimos.mapping.voxels",
Expand Down
76 changes: 76 additions & 0 deletions dimos/teleop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Teleop Stack

Teleoperation modules for DimOS. Currently supports Meta Quest 3 VR controllers.

## Architecture

```
Quest Browser (WebXR)
│ PoseStamped + Joy via WebSocket
Deno Bridge (teleop_server.ts)
│ LCM topics
QuestTeleopModule
│ WebXR → robot frame transform
│ Pose computation + button state packing
PoseStamped / TwistStamped / QuestButtons outputs
```

## Modules

### QuestTeleopModule
Base teleop module. Gets controller data, computes output poses, and publishes them. Default engage: hold primary button (X/A). Subclass to customize.

### ArmTeleopModule
Toggle-based engage — press primary button once to engage, press again to disengage.

### TwistTeleopModule
Outputs TwistStamped (linear + angular velocity) instead of PoseStamped.

### VisualizingTeleopModule
Adds Rerun visualization for debugging. Extends ArmTeleopModule (toggle engage).

## Subclassing

`QuestTeleopModule` is designed for extension. Override these methods:

| Method | Purpose |
|--------|---------|
| `_handle_engage()` | Customize engage/disengage logic |
| `_should_publish()` | Add conditions for when to publish |
| `_get_output_pose()` | Customize pose computation |
| `_publish_msg()` | Change output format |
| `_publish_button_state()` | Change button output |

### Rules for subclasses

- **Do not acquire `self._lock` in overrides.** The control loop already holds it.
Access `self._controllers`, `self._current_poses`, `self._is_engaged`, etc. directly.
- **Keep overrides fast** — they run inside the control loop at `control_loop_hz`.

## File Structure

```
teleop/
├── base/
│ └── teleop_protocol.py # TeleopProtocol interface
├── quest/
│ ├── quest_teleop_module.py # Base Quest teleop module
│ ├── quest_extensions.py # ArmTeleop, TwistTeleop, VisualizingTeleop
│ ├── quest_types.py # QuestControllerState, QuestButtons
│ └── web/ # Deno bridge + WebXR client
│ ├── teleop_server.ts
│ └── static/index.html
├── utils/
│ ├── teleop_transforms.py # WebXR → robot frame math
│ └── teleop_visualization.py # Rerun visualization helpers
└── blueprints.py # Module blueprints for easy instantiation
```

## Quick Start

See [Quest Web README](quest/web/README.md) for running the Deno bridge and connecting the Quest headset.
19 changes: 19 additions & 0 deletions dimos/teleop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025-2026 Dimensional Inc.
#
# 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.

"""Teleoperation modules for DimOS."""

from dimos.teleop.base import TeleopProtocol

__all__ = ["TeleopProtocol"]
19 changes: 19 additions & 0 deletions dimos/teleop/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025-2026 Dimensional Inc.
#
# 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.

"""Teleoperation protocol."""

from dimos.teleop.base.teleop_protocol import TeleopProtocol

__all__ = ["TeleopProtocol"]
58 changes: 58 additions & 0 deletions dimos/teleop/base/teleop_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# Copyright 2025-2026 Dimensional Inc.
#
# 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.

"""Teleoperation specifications: Protocol.

Defines the interface that all teleoperation modules must implement.
No implementation - just method signatures.
"""

from typing import Any, Protocol, runtime_checkable

# ============================================================================
# TELEOP PROTOCOL
# ============================================================================


@runtime_checkable
class TeleopProtocol(Protocol):
"""Protocol defining the teleoperation interface.

All teleop modules (Quest, keyboard, joystick, etc.) should implement these methods.
No state or implementation here - just the contract.
"""

# --- Lifecycle ---

def start(self) -> None:
"""Start the teleoperation module."""
...

def stop(self) -> None:
"""Stop the teleoperation module."""
...

# --- Engage / Disengage ---

def engage(self, hand: Any = None) -> bool:
"""Engage teleoperation. Hand type is device-specific (e.g., Hand enum for Quest)."""
...

def disengage(self, hand: Any = None) -> None:
"""Disengage teleoperation. Hand type is device-specific."""
...


__all__ = ["TeleopProtocol"]
51 changes: 51 additions & 0 deletions dimos/teleop/blueprints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# Copyright 2025-2026 Dimensional Inc.
#
# 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.

"""Teleop blueprints for testing and deployment."""

from dimos.core.blueprints import autoconnect
from dimos.core.transport import LCMTransport
from dimos.msgs.geometry_msgs import PoseStamped
from dimos.teleop.quest.quest_extensions import arm_teleop_module, visualizing_teleop_module
from dimos.teleop.quest.quest_types import QuestButtons

# -----------------------------------------------------------------------------
# Quest Teleop Blueprints
# -----------------------------------------------------------------------------

# Arm teleop with toggle-based engage
arm_teleop = autoconnect(
arm_teleop_module(),
).transports(
{
("left_controller_output", PoseStamped): LCMTransport("/teleop/left_delta", PoseStamped),
("right_controller_output", PoseStamped): LCMTransport("/teleop/right_delta", PoseStamped),
("buttons", QuestButtons): LCMTransport("/teleop/buttons", QuestButtons),
}
)

# Arm teleop with Rerun visualization
arm_teleop_visualizing = autoconnect(
visualizing_teleop_module(),
).transports(
{
("left_controller_output", PoseStamped): LCMTransport("/teleop/left_delta", PoseStamped),
("right_controller_output", PoseStamped): LCMTransport("/teleop/right_delta", PoseStamped),
("buttons", QuestButtons): LCMTransport("/teleop/buttons", QuestButtons),
}
)


__all__ = ["arm_teleop", "arm_teleop_visualizing"]
54 changes: 54 additions & 0 deletions dimos/teleop/quest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2025-2026 Dimensional Inc.
#
# 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.

"""Quest teleoperation module."""

from dimos.teleop.quest.quest_extensions import (
ArmTeleopModule,
TwistTeleopModule,
VisualizingTeleopModule,
arm_teleop_module,
twist_teleop_module,
visualizing_teleop_module,
)
from dimos.teleop.quest.quest_teleop_module import (
Hand,
QuestTeleopConfig,
QuestTeleopModule,
QuestTeleopStatus,
quest_teleop_module,
)
from dimos.teleop.quest.quest_types import (
QuestButtons,
QuestControllerState,
ThumbstickState,
)

__all__ = [
"ArmTeleopModule",
"Hand",
"QuestButtons",
"QuestControllerState",
"QuestTeleopConfig",
"QuestTeleopModule",
"QuestTeleopStatus",
"ThumbstickState",
"TwistTeleopModule",
"VisualizingTeleopModule",
# Blueprints
"arm_teleop_module",
"quest_teleop_module",
"twist_teleop_module",
"visualizing_teleop_module",
]
Loading
Loading