Skip to content
Open
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
9 changes: 5 additions & 4 deletions dimos/control/examples/twist_base_keyboard_teleop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
"""Keyboard teleop for twist base via ControlCoordinator.

Runs a mock holonomic twist base with pygame keyboard control.
WASD keys publish Twist → coordinator's twist_command port → virtual joints
The same bindings as `KeyboardTeleop` (arrows + WASD, Alt+A/D or Alt+←/→ strafe)
publish Twist → coordinator's twist_command port → virtual joints
→ tick loop → MockTwistBaseAdapter.

Controls:
W/S: Forward/backward (linear.x)
Q/E: Strafe left/right (linear.y)
A/D: Turn left/right (angular.z)
W/S and Up/Down: Forward/backward (linear.x)
A/D and Left/Right: Turn (angular.z)
Alt+A/D or Alt+←/→: Strafe left/right (linear.y)
Shift: 2x boost
Ctrl: 0.5x slow
Space: Emergency stop
Expand Down
52 changes: 36 additions & 16 deletions dimos/robot/unitree/keyboard_teleop.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,23 +133,39 @@ def _pygame_loop(self) -> None:
twist.linear = Vector3(0, 0, 0)
twist.angular = Vector3(0, 0, 0)

# Forward/backward (W/S)
if pygame.K_w in self._keys_held:
keys = self._keys_held

# Forward/backward — W/S or Up/Down (opposing keys cancel)
forward = pygame.K_w in keys or pygame.K_UP in keys
backward = pygame.K_s in keys or pygame.K_DOWN in keys
if forward and not backward:
twist.linear.x = self.linear_speed
if pygame.K_s in self._keys_held:
elif backward and not forward:
twist.linear.x = -self.linear_speed

# Strafe left/right (Q/E)
if pygame.K_q in self._keys_held:
twist.linear.y = self.linear_speed
if pygame.K_e in self._keys_held:
twist.linear.y = -self.linear_speed

# Turning (A/D)
if pygame.K_a in self._keys_held:
twist.angular.z = self.angular_speed
if pygame.K_d in self._keys_held:
twist.angular.z = -self.angular_speed
alt_held = pygame.K_LALT in keys or pygame.K_RALT in keys
alt_strafe = alt_held and (
pygame.K_a in keys
or pygame.K_d in keys
or pygame.K_LEFT in keys
or pygame.K_RIGHT in keys
)

# Alt + A/D or arrows: strafe (linear.y). Otherwise turn (yaw).
if alt_strafe:
strafe_left = pygame.K_a in keys or pygame.K_LEFT in keys
strafe_right = pygame.K_d in keys or pygame.K_RIGHT in keys
if strafe_left and not strafe_right:
twist.linear.y = self.linear_speed
elif strafe_right and not strafe_left:
twist.linear.y = -self.linear_speed
else:
turn_left = pygame.K_a in keys or pygame.K_LEFT in keys
turn_right = pygame.K_d in keys or pygame.K_RIGHT in keys
if turn_left and not turn_right:
twist.angular.z = self.angular_speed
elif turn_right and not turn_left:
twist.angular.z = -self.angular_speed

# Apply speed modifiers (Shift = boost, Ctrl = slow)
speed_multiplier = 1.0
Expand Down Expand Up @@ -196,7 +212,8 @@ def _update_display(self, twist: Twist) -> None:
f"Linear Y (Strafe L/R): {twist.linear.y:+.2f} m/s",
f"Angular Z (Turn L/R): {twist.angular.z:+.2f} rad/s",
"",
"Keys: " + ", ".join([pygame.key.name(k).upper() for k in self._keys_held if k < 256]),
"Keys: "
+ ", ".join(pygame.key.name(k).upper() for k in sorted(self._keys_held)),
]

for text in texts:
Expand All @@ -213,7 +230,7 @@ def _update_display(self, twist: Twist) -> None:

y_pos = 280
help_texts = [
"WS: Move | AD: Turn | QE: Strafe",
"W/S & Up/Dn: Move | A/D & L/R: Turn | Alt+A/D or Alt+L/R: Strafe",
"Shift: Boost | Ctrl: Slow",
"Space: E-Stop | ESC: Quit",
]
Expand All @@ -223,3 +240,6 @@ def _update_display(self, twist: Twist) -> None:
y_pos += 25

pygame.display.flip()


__all__ = ["KeyboardTeleop"]
2 changes: 2 additions & 0 deletions dimos/web/command-center-extension/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# command-center-extension

Keyboard teleop (after **Start Keyboard Control**): W/S and ↑/↓ forward/back, A/D and ←/→ turn, Alt+A/D or Alt+←/→ strafe, Space stop, Shift/Ctrl speed.

## Build and use

Install the Node dependencies:
Expand Down
57 changes: 33 additions & 24 deletions dimos/web/command-center-extension/src/KeyboardControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const controlKeys = new Set([
" ",
"Shift",
"Control",
"Alt",
]);

function isEditableTarget(target: EventTarget | null) {
Expand Down Expand Up @@ -52,32 +53,40 @@ function calculateVelocities(keys: Set<string>) {
return { linearX: 0, linearY: 0, angularY: 0, angularZ: 0 };
}

// Linear X (forward/backward) - W/S
if (keys.has("w")) {
const forward = keys.has("w") || keys.has("ArrowUp");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This control center extension is deprecated so no need to change it

const backward = keys.has("s") || keys.has("ArrowDown");

// Linear X (forward/backward) - W/S or ↑/↓
if (forward && !backward) {
linearX = linearSpeed * speedMultiplier;
} else if (keys.has("s")) {
} else if (backward && !forward) {
linearX = -linearSpeed * speedMultiplier;
}

// Angular Z (yaw/turn) - A/D
if (keys.has("a")) {
angularZ = angularSpeed * speedMultiplier;
} else if (keys.has("d")) {
angularZ = -angularSpeed * speedMultiplier;
}

// Linear Y (strafe) - Left/Right arrows
if (keys.has("ArrowLeft")) {
linearY = linearSpeed * speedMultiplier;
} else if (keys.has("ArrowRight")) {
linearY = -linearSpeed * speedMultiplier;
}

// Angular Y (pitch) - Up/Down arrows
if (keys.has("ArrowUp")) {
angularY = angularSpeed * speedMultiplier;
} else if (keys.has("ArrowDown")) {
angularY = -angularSpeed * speedMultiplier;
const altStrafe =
keys.has("Alt") &&
(keys.has("a") ||
keys.has("d") ||
keys.has("ArrowLeft") ||
keys.has("ArrowRight"));

// Alt + A/D or ←/→: strafe (linear Y). Otherwise those keys turn (yaw).
if (altStrafe) {
const strafeLeft = keys.has("a") || keys.has("ArrowLeft");
const strafeRight = keys.has("d") || keys.has("ArrowRight");
if (strafeLeft && !strafeRight) {
linearY = linearSpeed * speedMultiplier;
} else if (strafeRight && !strafeLeft) {
linearY = -linearSpeed * speedMultiplier;
}
} else {
const turnLeft = keys.has("a") || keys.has("ArrowLeft");
const turnRight = keys.has("d") || keys.has("ArrowRight");
if (turnLeft && !turnRight) {
angularZ = angularSpeed * speedMultiplier;
} else if (turnRight && !turnLeft) {
angularZ = -angularSpeed * speedMultiplier;
}
}
Comment on lines +66 to 90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Alt+A/D strafe silently broken on macOS browsers

On macOS, holding Alt and pressing A generates event.key === "å" (not "a"), and Alt+D generates "∂" instead of "d". Because neither character is in controlKeys, the KEYDOWN handler filters them out before they reach keysPressed. The result is that keys.has("a") is always false while Alt is held on macOS, so altStrafe only becomes true via ArrowLeft/ArrowRight — the A/D half of the advertised "Alt+A strafe left, Alt+D strafe right" feature never fires on macOS Chrome/Safari.


return { linearX, linearY, angularY, angularZ };
Expand Down Expand Up @@ -182,8 +191,8 @@ export default function KeyboardControlPanel({
{isActive && (
<div style={{ marginTop: 10, fontSize: 12, color: "#666" }}>
<div>Controls:</div>
<div>W/S: Forward/Backward | A/D: Turn</div>
<div>Arrows: Strafe/Pitch | Space: Stop</div>
<div>W/S or ↑/↓: Forward/Backward | A/D or ←/→: Turn</div>
<div>Alt+A/D or Alt+←/→: Strafe | Space: Stop</div>
<div>Shift: Boost | Ctrl: Slow</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion dimos/web/websocket_vis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Control:

- Set navigation goal
- Set GPS location goal
- Keyboard teleop (WASD)
- Keyboard teleop (W/S and arrows move, A/D and ←/→ turn, Alt+A/D or Alt+←/→ strafe — see command-center UI when enabled)
- Trigger exploration

## What it Provides
Expand Down