Skip to content

feat: add Doom-style teleop control module (#1401)#1554

Open
christiefhyang wants to merge 4 commits intodevfrom
christie/feat-doom-control
Open

feat: add Doom-style teleop control module (#1401)#1554
christiefhyang wants to merge 4 commits intodevfrom
christie/feat-doom-control

Conversation

@christiefhyang
Copy link
Member

@christiefhyang christiefhyang commented Mar 14, 2026

This PR adds a Doom-style teleoperation module that lets both Unitree Go2 and G1 be driven with a shared WSAD + mouse control scheme, modeled after first-person shooter (FPS) controls. The DoomTeleop module is robot-agnostic and only uses standard navigation interfaces: continuous velocity via /cmd_vel and optional discrete pose goals via /goal_pose and /cancel_goal.

The module is then integrated into the updated Unitree Go2 and G1 blueprint architecture so that both robots can use the same keyboard plus mouse teleop without changing any existing transport topics.

Motivation:

  • Provide a more natural, FPS-like teleop experience that combines keyboard and mouse, instead of pure keyboard-only teleop.
  • Unify teleop behavior across Go2 and G1 so that users can reuse the same mental model and input mapping on both platforms.
  • Keep the implementation robot-agnostic, exposing only standard navigation interfaces so it can be reused in future blueprints without further changes.

Implementation Details:

DoomTeleop Module:

  • Keyboard control (continuous velocity)
    • Maps keyboard input to Twist messages:
    • W / S: move forward / backward
    • A / D: turn left / right
    • Space: emergency stop (publishes a zero Twist and clears internal input state)
    • Publishes cmd_vel: Out[Twist], which is transported on the existing /cmd_vel channel in the blueprints.
    • When the window loses focus, the module clears its input state and sends a zero Twist so the robot does not keep moving on a stuck key.

  • Mouse control (yaw & optional pose)
    • Horizontal mouse motion adjusts yaw by adding to angular.z, providing an FPS-like look and turn behavior on top of keyboard turning.
    • Optional discrete pose helpers when navigation goal topics are wired:
    • Right-click: sends a small forward step goal (PoseStamped) in front of the current pose.
    • Middle-click: sends a small in-place rotation goal around the current position.
    • Uses the existing navigation goal interfaces:
    • goal_pose: Out[PoseStamped] mapped to /goal_pose
    • cancel_goal: Out[Bool] mapped to /cancel_goal
    • Space also cancels any active goal when cancel_goal is available.

  • Robot-agnostic design
    • DoomTeleop does not contain any Unitree- or robot-specific code.
    • It only publishes Twist, PoseStamped and Bool on the standard navigation interfaces, and can be reused with any robot that exposes /cmd_vel, /goal_pose and /cancel_goal in its transports.

  • Lifecycle and threading
    • Uses a background thread plus pygame to handle keyboard and mouse events asynchronously so teleop input does not block the rest of the system.
    • start and stop follow the existing Module lifecycle:
    • start calls super().start and spawns the input thread.
    • stop stops the thread, publishes a final zero Twist and cancels any active goal.

  • Go2 Blueprint Integration
    • Adds a go2_with_doom blueprint that composes the existing Unitree Go2 base navigation stack with doom_teleop.
    • Reuses the existing Go2 transports for:
    • /cmd_vel for velocity commands
    • /odom and navigation goal topics where available for optional pose goals
    • No changes are made to existing Go2 blueprints; go2_with_doom is an additive configuration.

  • G1 Blueprint Integration
    • Integrates doom_teleop into the updated modular G1 blueprint structure so it can be used alongside the existing joystick and keyboard teleop options.
    • Wires DoomTeleop only through the established /cmd_vel and navigation goal interfaces (goal_pose, cancel_goal, odom), matching the existing G1 navigation stack.
    • Existing G1 blueprints remain available and unchanged; Doom teleop is exposed as an additional configuration that can be selected when WSAD + mouse control is desired.

Testing

Simulation (MuJoCo)

Environment: Ubuntu or WSL with MuJoCo support.

Commands

Install dependencies:

uv sync --extra dev --extra cpu --extra sim

Go2

uv run dimos --simulation run unitree-go2 --extra-module doom_teleop

G1

uv run dimos --simulation run unitree-g1-joystick --extra-module doom_teleop

Verified

  • WSAD + mouse control the robot as expected
    • W / S: move forward / backward
    • A / D: turn left / right
    • Mouse motion: adds yaw to robot movement
  • Space and the mouse-based emergency behavior immediately stop the robot and clear motion commands.
  • When the teleop window loses focus, the module stops publishing motion commands so the robot does not continue moving on a stuck key.

IRL Test Plan (to be run by maintainers)

Robot

  • Unitree Go2 and/or G1

Command

Go2

dimos run unitree-go2 --extra-module doom_teleop

G1

dimos run unitree-g1-joystick --extra-module doom_teleop

Environment

Open indoor area with conservative speed limits on both the robot and the teleop module.


Checkpoints

  • Smooth start, turning, and stopping.
  • Emergency stop (Space / mouse) works immediately in any state.
  • No stuck-key behavior: releasing keys or losing window focus stops the robot instead of continuing to accelerate.

Breaking Changes

None. This PR introduces a new module that integrates with existing interfaces without modifying or breaking any existing control paths.

Contributor License Agreement

  • I have read and approved the CLA.

@christiefhyang christiefhyang marked this pull request as ready for review March 14, 2026 08:26
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR adds a new DOOM/FPS-style teleop module (DoomTeleop) at dimos/teleop/keyboard/doom_teleop.py and two new centralized blueprint files for Unitree G1 and Go2 robots that wire the teleop into existing navigation interfaces.

The DoomTeleop module itself is well-structured and follows existing patterns from KeyboardTeleop, but the two blueprint files have critical import errors that prevent them from loading at runtime:

  • Both blueprint files import doom_teleop from dimos.robot.doom_teleop, but the module actually lives at dimos.teleop.keyboard.doom_teleop — no file or re-export exists at the referenced path.
  • The G1 blueprint additionally imports keyboard_teleop, keyboard_pose_teleop, and g1_skills from dimos.robot.unitree_webrtc.* — paths that do not exist (the actual keyboard_teleop is at dimos.robot.unitree.keyboard_teleop, and the other two modules appear absent entirely).
  • Both blueprint files use n_dask_workers in .global_config() calls, but the GlobalConfig class only recognizes n_workers. Every other blueprint in the repo uses n_workers.

Confidence Score: 1/5

  • This PR will cause runtime ImportErrors in both blueprint files and should not be merged until the import paths and config keys are fixed.
  • The DoomTeleop module code is solid, but both blueprint files have broken imports that will crash at load time: doom_teleop is imported from a non-existent path, the G1 file references three additional non-existent modules, and all .global_config() calls use the wrong parameter name (n_dask_workers vs n_workers).
  • dimos/robot/unitree/g1/blueprints/unitree_g1_blueprints.py and dimos/robot/unitree/go2/blueprints/unitree_go2_blueprints.py both have broken imports and incorrect config keys that must be fixed before merge.

Important Files Changed

Filename Overview
dimos/teleop/keyboard/doom_teleop.py New DOOM-style teleop module; code is clean and follows existing KeyboardTeleop patterns well. No issues found in the module itself.
dimos/robot/unitree/g1/blueprints/unitree_g1_blueprints.py Multiple broken imports (doom_teleop, keyboard_teleop, keyboard_pose_teleop, g1_skills all reference wrong paths), and n_dask_workers used instead of n_workers throughout — file will not import at runtime.
dimos/robot/unitree/go2/blueprints/unitree_go2_blueprints.py Broken doom_teleop import (wrong path) and n_dask_workers used instead of n_workers — blueprint discovery will fail at import time.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph DoomTeleop["DoomTeleop Module"]
        KB["Keyboard Input\n(W/S/A/D/Space)"]
        MS["Mouse Input\n(Motion/Buttons)"]
        PG["Pygame Loop\n(50 Hz)"]
        KB --> PG
        MS --> PG
    end

    subgraph Outputs["Published Messages"]
        CV["cmd_vel\n(Twist)"]
        GP["goal_pose\n(PoseStamped)"]
        CG["cancel_goal\n(Bool)"]
    end

    subgraph Inputs["Subscriptions"]
        OD["odom\n(PoseStamped)"]
    end

    PG -->|"Continuous velocity"| CV
    PG -->|"RMB/MMB click"| GP
    PG -->|"Space e-stop"| CG
    OD -->|"Current pose for\nrelative goals"| PG

    subgraph Blueprints["Blueprint Integration"]
        G1["G1 Blueprint\n(with_joystick)"]
        GO2["Go2 Blueprint\n(go2_with_doom)"]
    end

    CV --> G1
    CV --> GO2
    GP --> G1
    GP --> GO2
Loading

Last reviewed commit: 985ab7e

from dimos.robot.unitree.connection.g1sim import g1_sim_connection
from dimos.robot.unitree_webrtc.keyboard_teleop import keyboard_teleop
from dimos.robot.unitree_webrtc.unitree_g1_skill_container import g1_skills
from dimos.utils.monitoring import utilization
Copy link
Contributor

Choose a reason for hiding this comment

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

Broken import — module path does not exist

The file dimos/robot/doom_teleop.py does not exist. The DoomTeleop module was added at dimos/teleop/keyboard/doom_teleop.py, so this import will raise ModuleNotFoundError at runtime.

Suggested change
from dimos.utils.monitoring import utilization
from dimos.teleop.keyboard.doom_teleop import doom_teleop

The same issue is present in dimos/robot/unitree/go2/blueprints/unitree_go2_blueprints.py:66.

Comment on lines +62 to +64
from dimos.robot.unitree.connection.g1sim import g1_sim_connection
from dimos.robot.unitree_webrtc.keyboard_teleop import keyboard_teleop
from dimos.robot.unitree_webrtc.unitree_g1_skill_container import g1_skills
Copy link
Contributor

Choose a reason for hiding this comment

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

Broken imports — modules not found at these paths

These three imports reference dimos.robot.unitree_webrtc.*, but the actual modules live under different paths:

  • keyboard_teleop exists at dimos.robot.unitree.keyboard_teleop (see unitree_g1_joystick.py for the correct import).
  • keyboard_pose_teleop and unitree_g1_skill_container do not appear to exist anywhere in the codebase.

This file will fail to import entirely.

Suggested change
from dimos.robot.unitree.connection.g1sim import g1_sim_connection
from dimos.robot.unitree_webrtc.keyboard_teleop import keyboard_teleop
from dimos.robot.unitree_webrtc.unitree_g1_skill_container import g1_skills
from dimos.robot.unitree.keyboard_teleop import keyboard_teleop

foxglove_bridge(),
)
.global_config(n_dask_workers=4, robot_model="unitree_g1")
.transports(
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong config key — n_dask_workers should be n_workers

GlobalConfig defines the field as n_workers (see dimos/core/global_config.py:38), and all existing blueprints in the repository use n_workers. The key n_dask_workers will be silently ignored (passed as **kwargs) or raise an error, meaning blueprints will fall back to the default of 2 workers instead of the intended count.

This affects every .global_config(n_dask_workers=...) call in both new blueprint files (G1 lines 92, 139, 144, 203; Go2 lines 101, 114, 170).

Suggested change
.transports(
.global_config(n_workers=4, robot_model="unitree_g1")

from dimos.robot.unitree.connection.go2 import GO2Connection, go2_connection
from dimos.robot.unitree_webrtc.unitree_skill_container import unitree_skills
from dimos.utils.monitoring import utilization
from dimos.web.websocket_vis.websocket_vis_module import websocket_vis
Copy link
Contributor

Choose a reason for hiding this comment

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

Broken import — module path does not exist

Same issue as in the G1 blueprint: dimos/robot/doom_teleop.py does not exist. The module lives at dimos/teleop/keyboard/doom_teleop.py.

Suggested change
from dimos.web.websocket_vis.websocket_vis_module import websocket_vis
from dimos.teleop.keyboard.doom_teleop import doom_teleop

("world/robot/camera", "camera_optical", GO2Connection.camera_info_static),
],
),
).global_config(n_dask_workers=4, robot_model="unitree_go2")
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong config key — n_dask_workers should be n_workers

GlobalConfig.n_workers is the correct field (see dimos/core/global_config.py:38). n_dask_workers is not a recognized config key. Every existing blueprint in the repo uses n_workers. This affects lines 101, 114, and 170 in this file.

Suggested change
).global_config(n_dask_workers=4, robot_model="unitree_go2")
).global_config(n_workers=4, robot_model="unitree_go2")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant