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
14 changes: 12 additions & 2 deletions scripts/environments/teleoperation/teleop_se3_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"lekiwi-keyboard",
"lekiwi-gamepad",
"lekiwi-leader",
"s30-keyboard",
"s30-gamepad",
],
help="Device for interacting with environment",
)
Expand Down Expand Up @@ -281,10 +283,18 @@ def main(): # noqa: C901
from leisaac.devices import LeKiwiGamepad

teleop_interface = LeKiwiGamepad(env, sensitivity=args_cli.sensitivity)
elif args_cli.teleop_device == "s30-keyboard":
from leisaac.devices import S30Keyboard

teleop_interface = S30Keyboard(env, sensitivity=args_cli.sensitivity)
elif args_cli.teleop_device == "s30-gamepad":
from leisaac.devices import SO101Gamepad

teleop_interface = SO101Gamepad(env, sensitivity=args_cli.sensitivity)
else:
raise ValueError(
f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'gamepad', 'so101leader',"
" 'bi-so101leader', 'lekiwi-keyboard', 'lekiwi-leader', 'lekiwi-gamepad'."
f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'gamepad', 's30-keyboard',"
" 's30-gamepad', 'so101leader', 'bi-so101leader', 'lekiwi-keyboard', 'lekiwi-leader', 'lekiwi-gamepad'."
)

# add teleoperation key for env reset
Expand Down
113 changes: 113 additions & 0 deletions source/leisaac/leisaac/assets/robots/elfin_s30.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from pathlib import Path

import isaaclab.sim as sim_utils
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.assets.articulation import ArticulationCfg
from leisaac.utils.constant import ASSETS_ROOT

"""Configuration for the Elfin S30 arm + Robotiq 2F-140 gripper."""

S30_ASSET_PATH = str(Path(ASSETS_ROOT) / "robots" / "elfin_s30" / "S30.usd")

S30_CFG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=S30_ASSET_PATH,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=True,
solver_position_iteration_count=8,
solver_velocity_iteration_count=4,
fix_root_link=True,
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(2.2, -1.5, 0.89),
rot=(0.0, 0.0, 0.0, 1.0),
joint_pos={
"joint1": 0.0,
"joint2": -1.5708, # -90 deg
"joint3": 1.5708, # +90 deg
"joint4": 0.0,
"joint5": -1.5708, # -90 deg
"joint6": 0.0,
"finger_joint": 0.0,
},
),
actuators={
"elfin-arm": ImplicitActuatorCfg(
joint_names_expr=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"],
effort_limit_sim=350.0,
velocity_limit_sim=1.57,
stiffness=349.0,
damping=34.9,
),
"robotiq-finger": ImplicitActuatorCfg(
joint_names_expr=["finger_joint"],
effort_limit_sim=50.0,
velocity_limit_sim=0.5,
stiffness=800.0,
damping=40.0,
),
# ActionGraph drove ONLY right_outer_knuckle_joint at the same 0-45° as finger_joint.
# Ratio = +1 (same direction): both joints share identical position targets.
# All other 4-bar joints (inner_knuckle, outer_finger, inner_finger) are
# handled by PhysX gear/mimic constraints embedded in physics_edit.usd.
# inner_finger_pad_joint is NVIDIA-specific (absent from standard URDF mimic list),
# so it has no PhysX constraint and must be tracked here with ratio = -1.
"robotiq-mimic": ImplicitActuatorCfg(
joint_names_expr=[
"right_outer_knuckle_joint",
"left_inner_finger_pad_joint",
"right_inner_finger_pad_joint",
],
effort_limit_sim=50.0,
velocity_limit_sim=0.5,
stiffness=800.0,
damping=40.0,
),
},
soft_joint_pos_limit_factor=1.0,
)

# Joint limits as stored in USD (degrees).
# Derived from URDF limits converted to degrees:
# joint1: ±6.28 rad → ±360°
# joint2: -3.3158 ~ 0.1745 rad → -190° ~ 10°
# joint3: ±2.9319 rad → ±168°
# joint4/5/6: ±6.28 rad → ±360°
# finger_joint: 0 ~ 45° (Robotiq 2F-140 opening angle)
S30_USD_JOINT_LIMITS = {
"joint1": (-360.0, 360.0),
"joint2": (-190.0, 10.0),
"joint3": (-168.0, 168.0),
"joint4": (-360.0, 360.0),
"joint5": (-360.0, 360.0),
"joint6": (-360.0, 360.0),
"finger_joint": ( 0.0, 45.0),
}

# Motor limits — the angle range (degrees) accepted by the TCP/IP SDK.
# For S30 the SDK takes joint angles directly in degrees, so limits equal USD limits.
S30_MOTOR_LIMITS = {
"joint1": (-360.0, 360.0),
"joint2": (-190.0, 10.0),
"joint3": (-168.0, 168.0),
"joint4": (-360.0, 360.0),
"joint5": (-360.0, 360.0),
"joint6": (-360.0, 360.0),
"finger_joint": ( 0.0, 45.0),
}

# Rest pose range (degrees) used to detect whether the arm has returned to home.
# Centred on the init_state joint positions converted to degrees.
S30_REST_POSE_RANGE = {
"joint1": ( 0.0 - 30.0, 0.0 + 30.0),
"joint2": (-90.0 - 30.0, -90.0 + 30.0),
"joint3": ( 90.0 - 30.0, 90.0 + 30.0),
"joint4": ( 0.0 - 30.0, 0.0 + 30.0),
"joint5": (-90.0 - 30.0, -90.0 + 30.0),
"joint6": ( 0.0 - 30.0, 0.0 + 30.0),
"finger_joint": ( 0.0, 15.0), # nearly open counts as rest
}
4 changes: 3 additions & 1 deletion source/leisaac/leisaac/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
if TYPE_CHECKING:
from .device_base import DeviceBase
from .gamepad import SO101Gamepad
from .keyboard import SO101Keyboard
from .keyboard import S30Keyboard, SO101Keyboard
from .lekiwi import LeKiwiGamepad, LeKiwiKeyboard, LeKiwiLeader
from .lerobot import BiSO101Leader, SO101Leader, SO101LeaderRemote

__all__ = [
"DeviceBase",
"S30Keyboard",
"SO101Gamepad",
"SO101Keyboard",
"LeKiwiGamepad",
Expand All @@ -22,6 +23,7 @@

_LAZY_IMPORTS = {
"DeviceBase": (".device_base", "DeviceBase"),
"S30Keyboard": (".keyboard", "S30Keyboard"),
"SO101Gamepad": (".gamepad", "SO101Gamepad"),
"SO101Keyboard": (".keyboard", "SO101Keyboard"),
"LeKiwiGamepad": (".lekiwi", "LeKiwiGamepad"),
Expand Down
28 changes: 27 additions & 1 deletion source/leisaac/leisaac/devices/action_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@

def init_action_cfg(action_cfg, device):
"""SO101 Follower action configuration: arm_action and gripper_action"""
if device in ["so101leader", "lekiwi-leader"]:
if device in ["s30"]:
action_cfg.arm_action = mdp.JointPositionActionCfg(
asset_name="robot",
joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"],
scale=1.0,
)
action_cfg.gripper_action = mdp.JointPositionActionCfg(
asset_name="robot",
joint_names=["finger_joint"],
scale=1.0,
)
elif device in ["so101leader", "lekiwi-leader"]:
action_cfg.arm_action = mdp.JointPositionActionCfg(
asset_name="robot",
joint_names=["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll"],
Expand All @@ -19,6 +30,18 @@ def init_action_cfg(action_cfg, device):
joint_names=["gripper"],
scale=1.0,
)
elif device in ["s30-keyboard", "s30-gamepad"]:
action_cfg.arm_action = mdp.DifferentialInverseKinematicsActionCfg(
asset_name="robot",
joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"],
body_name="elfin_link6",
controller=mdp.DifferentialIKControllerCfg(command_type="pose", ik_method="dls", use_relative_mode=True),
)
action_cfg.gripper_action = mdp.RelativeJointPositionActionCfg(
asset_name="robot",
joint_names=["finger_joint"],
scale=1.0,
)
elif device in ["keyboard", "gamepad", "lekiwi-keyboard", "lekiwi-gamepad"]:
action_cfg.arm_action = mdp.DifferentialInverseKinematicsActionCfg(
asset_name="robot",
Expand Down Expand Up @@ -169,6 +192,9 @@ def preprocess_device_action(action: dict[str, Any], teleop_device) -> torch.Ten
processed_action = convert_action_from_so101_leader(
action["joint_state"], action["motor_limits"], teleop_device
)
elif action.get("s30-keyboard") is not None or action.get("s30-gamepad") is not None:
processed_action = torch.zeros(teleop_device.env.num_envs, 7, device=teleop_device.env.device)
processed_action[:, :] = action["joint_state"]
elif action.get("keyboard") is not None or action.get("gamepad") is not None:
processed_action = torch.zeros(teleop_device.env.num_envs, 8, device=teleop_device.env.device)
processed_action[:, :] = action["joint_state"]
Expand Down
1 change: 1 addition & 0 deletions source/leisaac/leisaac/devices/keyboard/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .s30_keyboard import S30Keyboard
from .so101_keyboard import SO101Keyboard
104 changes: 104 additions & 0 deletions source/leisaac/leisaac/devices/keyboard/s30_keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import carb
import numpy as np

from ..device_base import Device


class S30Keyboard(Device):
"""键盘控制器,用于 S30 + Robotiq 2F-140 机械臂的末端执行器控制。

发送 SE(3) 增量位姿指令,通过差分逆运动学(DiffIK)驱动 S30 六轴臂,
同时独立控制夹爪。

Key bindings:
============================== ================= =================
Description Key Key
============================== ================= =================
Forward / Backward W S
Left / Right A D
Up / Down Q E
Rotate (Yaw) Left / Right J L
Rotate (Pitch) Up / Down K I
Gripper Open / Close U O
============================== ================= =================

Output (7D): [dx, dy, dz, droll, dpitch, dyaw, d_gripper]
"""

def __init__(self, env, sensitivity: float = 1.0):
super().__init__(env, "s30-keyboard")

self.pos_sensitivity = 0.03 * sensitivity
self.rot_sensitivity = 0.30 * sensitivity
self.joint_sensitivity = 0.30 * sensitivity

self._create_key_bindings()

# 7D: (dx, dy, dz, droll, dpitch, dyaw, d_gripper)
self._delta_action = np.zeros(7)

self.asset_name = "robot"
self.robot_asset = self.env.scene[self.asset_name]

self.target_frame = "elfin_link6"
body_idxs, _ = self.robot_asset.find_bodies(self.target_frame)
self.target_frame_idx = body_idxs[0]

def _add_device_control_description(self):
self._display_controls_table.add_row(["W", "forward"])
self._display_controls_table.add_row(["S", "backward"])
self._display_controls_table.add_row(["A", "left"])
self._display_controls_table.add_row(["D", "right"])
self._display_controls_table.add_row(["Q", "up"])
self._display_controls_table.add_row(["E", "down"])
self._display_controls_table.add_row(["J", "rotate_left (yaw)"])
self._display_controls_table.add_row(["L", "rotate_right (yaw)"])
self._display_controls_table.add_row(["K", "rotate_down (pitch)"])
self._display_controls_table.add_row(["I", "rotate_up (pitch)"])
self._display_controls_table.add_row(["U", "gripper_open"])
self._display_controls_table.add_row(["O", "gripper_close"])

def get_device_state(self):
return self._convert_delta_from_frame(self._delta_action)

def reset(self):
self._delta_action[:] = 0.0

def _on_keyboard_event(self, event, *args, **kwargs):
super()._on_keyboard_event(event, *args, **kwargs)
if event.type == carb.input.KeyboardEventType.KEY_PRESS:
if event.input.name in self._INPUT_KEY_MAPPING:
self._delta_action += self._ACTION_DELTA_MAPPING[self._INPUT_KEY_MAPPING[event.input.name]]
if event.type == carb.input.KeyboardEventType.KEY_RELEASE:
if event.input.name in self._INPUT_KEY_MAPPING:
self._delta_action -= self._ACTION_DELTA_MAPPING[self._INPUT_KEY_MAPPING[event.input.name]]

def _create_key_bindings(self):
self._ACTION_DELTA_MAPPING = {
"forward": np.asarray([0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0]) * self.pos_sensitivity,
"backward": np.asarray([0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]) * self.pos_sensitivity,
"left": np.asarray([-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) * self.pos_sensitivity,
"right": np.asarray([ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) * self.pos_sensitivity,
"up": np.asarray([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]) * self.pos_sensitivity,
"down": np.asarray([0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0]) * self.pos_sensitivity,
"rotate_left": np.asarray([0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0]) * self.rot_sensitivity,
"rotate_right": np.asarray([0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0]) * self.rot_sensitivity,
"rotate_up": np.asarray([0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0]) * self.rot_sensitivity,
"rotate_down": np.asarray([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0]) * self.rot_sensitivity,
"gripper_open": np.asarray([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]) * self.joint_sensitivity,
"gripper_close": np.asarray([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0]) * self.joint_sensitivity,
}
self._INPUT_KEY_MAPPING = {
"W": "forward",
"S": "backward",
"A": "left",
"D": "right",
"Q": "up",
"E": "down",
"J": "rotate_left",
"L": "rotate_right",
"K": "rotate_down",
"I": "rotate_up",
"U": "gripper_open",
"O": "gripper_close",
}
2 changes: 1 addition & 1 deletion source/leisaac/leisaac/policy/lerobot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def create_module_hierarchy(path: str):
setattr(sys.modules[parent_path], parts[i - 1], mod)


helpers_path = "lerobot.scripts.server.helpers"
helpers_path = "lerobot.async_inference.helpers"
create_module_hierarchy(helpers_path)

fake_lerobot_module = sys.modules[helpers_path]
Expand Down
3 changes: 2 additions & 1 deletion source/leisaac/leisaac/policy/lerobot/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum

import torch
Expand All @@ -25,6 +25,7 @@ class RemotePolicyConfig:
lerobot_features: dict[str, PolicyFeature]
actions_per_chunk: int
device: str = "cpu"
rename_map: dict = field(default_factory=dict)


RawObservation = dict[str, torch.Tensor]
Expand Down
Loading