From 01e8fe8f72c3ea1480d87a57c7a6072129651e3b Mon Sep 17 00:00:00 2001 From: Mamadou Ly Date: Fri, 8 May 2026 22:28:16 -0400 Subject: [PATCH 1/2] allowing strafe for alt + L/R arrows --- dimos/robot/unitree/keyboard_teleop.py | 52 +++++++++++------ .../src/KeyboardControlPanel.tsx | 57 +++++++++++-------- dimos/web/websocket_vis/README.md | 2 +- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/dimos/robot/unitree/keyboard_teleop.py b/dimos/robot/unitree/keyboard_teleop.py index e3c78ecc52..cc7b890073 100644 --- a/dimos/robot/unitree/keyboard_teleop.py +++ b/dimos/robot/unitree/keyboard_teleop.py @@ -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 @@ -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: @@ -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", ] @@ -223,3 +240,6 @@ def _update_display(self, twist: Twist) -> None: y_pos += 25 pygame.display.flip() + + +__all__ = ["KeyboardTeleop"] diff --git a/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx b/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx index 8754b0f4e8..c4f55beec6 100644 --- a/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx +++ b/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx @@ -23,6 +23,7 @@ const controlKeys = new Set([ " ", "Shift", "Control", + "Alt", ]); function isEditableTarget(target: EventTarget | null) { @@ -52,32 +53,40 @@ function calculateVelocities(keys: Set) { 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"); + 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; + } } return { linearX, linearY, angularY, angularZ }; @@ -182,8 +191,8 @@ export default function KeyboardControlPanel({ {isActive && (
Controls:
-
W/S: Forward/Backward | A/D: Turn
-
Arrows: Strafe/Pitch | Space: Stop
+
W/S or ↑/↓: Forward/Backward | A/D or ←/→: Turn
+
Alt+A/D or Alt+←/→: Strafe | Space: Stop
Shift: Boost | Ctrl: Slow
)} diff --git a/dimos/web/websocket_vis/README.md b/dimos/web/websocket_vis/README.md index 3f0776bed5..e23e76b6d0 100644 --- a/dimos/web/websocket_vis/README.md +++ b/dimos/web/websocket_vis/README.md @@ -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 From d3160854c4d25cb94df1aaf3866e7f4694f204c3 Mon Sep 17 00:00:00 2001 From: Mamadou Ly Date: Fri, 8 May 2026 22:43:15 -0400 Subject: [PATCH 2/2] update docs --- dimos/control/examples/twist_base_keyboard_teleop.py | 9 +++++---- dimos/web/command-center-extension/README.md | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dimos/control/examples/twist_base_keyboard_teleop.py b/dimos/control/examples/twist_base_keyboard_teleop.py index 43b2270410..3db715bffd 100644 --- a/dimos/control/examples/twist_base_keyboard_teleop.py +++ b/dimos/control/examples/twist_base_keyboard_teleop.py @@ -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 diff --git a/dimos/web/command-center-extension/README.md b/dimos/web/command-center-extension/README.md index 5364ffe4e1..81beb00d70 100644 --- a/dimos/web/command-center-extension/README.md +++ b/dimos/web/command-center-extension/README.md @@ -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: