Skip to content

feat(gamepad): add direct joint control for Xbox/HID gamepad with SO100/SO101#3174

Open
slurms wants to merge 1 commit intohuggingface:mainfrom
slurms:feat/gamepad-direct-joint-control
Open

feat(gamepad): add direct joint control for Xbox/HID gamepad with SO100/SO101#3174
slurms wants to merge 1 commit intohuggingface:mainfrom
slurms:feat/gamepad-direct-joint-control

Conversation

@slurms
Copy link

@slurms slurms commented Mar 17, 2026

Title

feat(gamepad): add direct joint control for Xbox/HID gamepad with SO100/SO101

Type / Scope

  • Type: Feature
  • Scope: teleoperators/gamepad, processor

Summary / Motivation

Adds support for teleoperating SO100/SO101 follower robots directly from an Xbox controller (or any HID gamepad) by mapping each gamepad axis to an individual joint servo.

Previously, gamepad teleoperation required an inverse kinematics pipeline with a URDF and the placo C++ dependency.

This approach is simpler, has zero extra dependencies, and gives the operator direct, predictable control over each joint.

The control mapping is:

  • left stick Y → shoulder_lift,
  • left stick X → shoulder_pan,
  • right stick Y → elbow_flex,
  • right stick X → wrist_flex,
  • LB/RB → wrist_roll,
  • LT/RT → gripper.

This was intuitive enough for my 5 year old to master when playing with it locally.

Related issues

What changed

  • gamepad_utils.py: Added Xbox One GIP HID report parser (_update_xbox, see docs) with 16-bit LE stick/trigger parsing and Y-axis sign normalization to match SDL convention. Replaced string-based gamepad detection with HID usage_page/usage matching. Added wrist_roll_command (LB/RB) and right_x to base InputController. Fixed get_deltas() axis mapping so left stick Y → delta_x and left stick X → delta_y.
  • configuration_gamepad.py: Added device_name: str | None config for selecting a specific gamepad by product/manufacturer substring.
  • teleop_gamepad.py: Action dict now includes delta_wx (right stick X) and delta_wz (wrist roll).
  • delta_action_processor.py: Added MapGamepadToJointPositionsStep processor that reads current joint positions from observation and applies scaled gamepad deltas. Also added delta_wz passthrough to existing MapDeltaActionToRobotActionStep.
  • factory.py: Extended make_default_processors() to accept optional teleop_config/robot_config params. When a delta teleop + SO follower combination is detected, it swaps in the joint control processor automatically.
  • lerobot_teleoperate.py: Calls make_default_processors(cfg.teleop, cfg.robot).
  • No breaking changes. Existing make_default_processors() with no args behaves identically to before.

How was this tested (or how to run locally)

Manual tested by teleoperating an SO101 follower with an Xbox One controller over USB on macOS using:

lerobot-teleoperate \
   --robot.type=so101_follower \
   --robot.port=/dev/tty.usbmodem58760431541 \
   --teleop.type=gamepad

Run tests with

pytest -v tests/teleoperators/test_gamepad_controller.py tests/processor/test_gamepad_joint_processor.py
  • Tests added:
    • tests/teleoperators/test_gamepad_controller.py
    • tests/processor/test_gamepad_joint_processor.py

Checklist (required before merge)

  • Linting/formatting run (pre-commit run -a)
  • All tests pass locally (pytest)
  • Documentation updated
  • CI is green

Reviewer notes

  • The _update_xbox() parser handles the Xbox One GIP protocol (18-byte reports with 0x20 header). Other Xbox-compatible controllers using this same protocol over USB should work. The existing _update_logitech() parser is preserved for Logitech RumblePad 2 and similar 8-byte HID reports.
  • joint_step_size defaults to 3.0°/frame. At 60fps this gives ~180°/s at full stick deflection. This is tunable via the MapGamepadToJointPositionsStep dataclass field.
  • On macOS, the HID path is used instead of pygame because pygame doesn't reliably detect Xbox controller input. The GamepadController (pygame) path on Linux is unchanged but now also has right_x and wrist_roll_command attributes from the base class.
  • The device_name config option is useful when multiple HID devices are connected (e.g., webcams that register as HID devices - my Logitech Streamcam was detected when using --teleop.type=gamepad for some reason, hence adding this).

@github-actions github-actions bot added documentation Improvements or fixes to the project’s docs tests Problems with test coverage, failures, or improvements to testing robots Issues concerning robots HW interfaces processor Issue related to processor labels Mar 17, 2026
…00/SO101

Add support for controlling SO100/SO101 follower robots directly from
an Xbox controller (or any HID gamepad) by mapping each gamepad axis
to an individual joint servo, bypassing the IK pipeline.

Gamepad axis mapping:
  - Left stick Y  → shoulder_lift
  - Left stick X  → shoulder_pan
  - Right stick Y → elbow_flex
  - Right stick X → wrist_flex
  - LB/RB         → wrist_roll
  - LT/RT         → gripper (close/open)

Changes:
- Add Xbox One GIP HID report parser (_update_xbox) with proper 16-bit
  LE stick/trigger parsing and Y-axis sign normalization
- Add HID usage_page/usage-based gamepad detection (replaces string matching)
- Add device_name config option for selecting a specific gamepad
- Add MapGamepadToJointPositionsStep processor that reads current joint
  positions from observation and applies scaled gamepad deltas
- Add make_processors() factory that auto-selects the direct joint
  pipeline for gamepad/keyboard_ee + SO follower combinations
- Wire make_processors() into teleoperate and record scripts
@slurms slurms force-pushed the feat/gamepad-direct-joint-control branch from aec8ef6 to 3b19515 Compare March 17, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or fixes to the project’s docs processor Issue related to processor robots Issues concerning robots HW interfaces tests Problems with test coverage, failures, or improvements to testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some teleoperators that control an end effector don't work

1 participant