Skip to content

Commit 41aeb55

Browse files
authored
Merge pull request #172 from utn-mi/gamal/tilburg-hand
feat(env): tilburg hand controller integration
2 parents 8f543e0 + 3b0e9f8 commit 41aeb55

10 files changed

Lines changed: 406 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies = ["websockets>=11.0",
2727
"mujoco==3.2.6",
2828
# NOTE: Same for pinocchio (pin)
2929
"pin==2.7.0",
30+
"tilburg-hand",
3031
]
3132
readme = "README.md"
3233
maintainers = [
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import logging
2+
3+
from rcs._core.common import RobotPlatform
4+
from rcs.control.fr3_desk import FCI, ContextManager, Desk, load_creds_fr3_desk
5+
from rcs.envs.base import ControlMode, RelativeTo
6+
from rcs.envs.creators import RCSFR3EnvCreator, RCSSimEnvCreator
7+
from rcs.envs.utils import (
8+
default_fr3_hw_robot_cfg,
9+
default_fr3_sim_gripper_cfg,
10+
default_fr3_sim_robot_cfg,
11+
default_mujoco_cameraset_cfg,
12+
default_tilburg_hw_hand_cfg,
13+
)
14+
15+
logger = logging.getLogger(__name__)
16+
logger.setLevel(logging.INFO)
17+
18+
ROBOT_IP = "192.168.101.1"
19+
ROBOT_INSTANCE = RobotPlatform.SIMULATION
20+
21+
22+
"""
23+
Create a .env file in the same directory as this file with the following content:
24+
FR3_USERNAME=<username on franka desk>
25+
FR3_PASSWORD=<password on franka desk>
26+
27+
When you use a real FR3 you first need to unlock its joints using the following cli script:
28+
29+
python -m rcs fr3 unlock <ip>
30+
31+
or put it into guiding mode using:
32+
33+
python -m rcs fr3 guiding-mode <ip>
34+
35+
When you are done you lock it again using:
36+
37+
python -m rcs fr3 lock <ip>
38+
39+
or even shut it down using:
40+
41+
python -m rcs fr3 shutdown <ip>
42+
"""
43+
44+
45+
def main():
46+
resource_manger: FCI | ContextManager
47+
if ROBOT_INSTANCE == RobotPlatform.HARDWARE:
48+
user, pw = load_creds_fr3_desk()
49+
resource_manger = FCI(Desk(ROBOT_IP, user, pw), unlock=False, lock_when_done=False)
50+
else:
51+
resource_manger = ContextManager()
52+
with resource_manger:
53+
if ROBOT_INSTANCE == RobotPlatform.HARDWARE:
54+
env_rel = RCSFR3EnvCreator()(
55+
ip=ROBOT_IP,
56+
control_mode=ControlMode.CARTESIAN_TQuat,
57+
robot_cfg=default_fr3_hw_robot_cfg(),
58+
collision_guard="lab",
59+
gripper_cfg=default_tilburg_hw_hand_cfg(),
60+
max_relative_movement=0.5,
61+
relative_to=RelativeTo.LAST_STEP,
62+
)
63+
else:
64+
env_rel = RCSSimEnvCreator()(
65+
control_mode=ControlMode.CARTESIAN_TQuat,
66+
robot_cfg=default_fr3_sim_robot_cfg(),
67+
collision_guard=False,
68+
gripper_cfg=default_fr3_sim_gripper_cfg(),
69+
camera_set_cfg=default_mujoco_cameraset_cfg(),
70+
max_relative_movement=0.5,
71+
relative_to=RelativeTo.LAST_STEP,
72+
)
73+
env_rel.get_wrapper_attr("sim").open_gui()
74+
75+
env_rel.reset()
76+
print(env_rel.unwrapped.robot.get_cartesian_position()) # type: ignore
77+
close_action = 0
78+
open_action = 1
79+
80+
for _ in range(10):
81+
for _ in range(10):
82+
# move 1cm in x direction (forward) and close gripper
83+
act = {"tquat": [0.01, 0, 0, 0, 0, 0, 1], "hand": close_action}
84+
obs, reward, terminated, truncated, info = env_rel.step(act)
85+
if truncated or terminated:
86+
logger.info("Truncated or terminated!")
87+
return
88+
from time import sleep
89+
90+
sleep(5)
91+
for _ in range(10):
92+
# move 1cm in negative x direction (backward) and open gripper
93+
act = {"tquat": [-0.01, 0, 0, 0, 0, 0, 1], "hand": open_action}
94+
obs, reward, terminated, truncated, info = env_rel.step(act)
95+
if truncated or terminated:
96+
logger.info("Truncated or terminated!")
97+
return
98+
99+
100+
if __name__ == "__main__":
101+
main()

python/rcs/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import site
55

66
from gymnasium import register
7-
from rcs import camera, control, envs, sim
7+
from rcs import camera, control, envs, hand, sim
88
from rcs._core import __version__, common, hw
99
from rcs.envs.creators import (
1010
FR3LabPickUpSimDigitHandEnvCreator,
@@ -20,7 +20,7 @@
2020
}
2121

2222
# make submodules available
23-
__all__ = ["__doc__", "__version__", "common", "hw", "sim", "camera", "scenes", "control", "envs"]
23+
__all__ = ["__doc__", "__version__", "common", "hw", "sim", "camera", "scenes", "control", "envs", "hand"]
2424

2525
# register gymnasium environments
2626
register(

python/rcs/envs/base.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
RCSpaceType,
1515
Vec6Type,
1616
Vec7Type,
17+
Vec18Type,
1718
VecType,
1819
get_space,
1920
get_space_keys,
2021
)
22+
from rcs.hand.interface import BaseHand
2123

2224
_logger = logging.getLogger(__name__)
2325

@@ -99,6 +101,22 @@ class GripperDictType(RCSpaceType):
99101
gripper: Annotated[float, gym.spaces.Box(low=0, high=1, dtype=np.float32)]
100102

101103

104+
class HandBinDictType(RCSpaceType):
105+
# 0 for closed, 1 for open (>=0.5 for open)
106+
hand: Annotated[float, gym.spaces.Box(low=0, high=1, dtype=np.float32)]
107+
108+
109+
class HandVecDictType(RCSpaceType):
110+
hand: Annotated[
111+
Vec18Type,
112+
gym.spaces.Box(
113+
low=np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
114+
high=np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]),
115+
dtype=np.float32,
116+
),
117+
]
118+
119+
102120
class CameraDictType(RCSpaceType):
103121
frames: dict[
104122
Annotated[str, "camera_names"],
@@ -627,3 +645,74 @@ def action(self, action: dict[str, Any]) -> dict[str, Any]:
627645
self._last_gripper_cmd = gripper_action
628646
del action[self.gripper_key]
629647
return action
648+
649+
650+
class HandWrapper(ActObsInfoWrapper):
651+
"""
652+
This wrapper allows for controlling the hand of the robot
653+
using either binary or continuous actions.
654+
The binary action space allows for opening and closing the hand,
655+
while the continuous action space allows for setting the hand
656+
to a specific pose.
657+
The wrapper also provides an observation space that includes
658+
the hand state.
659+
The hand state is represented as a binary value (0 for closed,
660+
1 for open) or as a continuous value (normalized joint positions).
661+
"""
662+
663+
BINARY_HAND_CLOSED = 0
664+
BINARY_HAND_OPEN = 1
665+
666+
def __init__(self, env, hand: BaseHand, binary: bool = True):
667+
super().__init__(env)
668+
self.unwrapped: RobotEnv
669+
self.observation_space: gym.spaces.Dict
670+
self.action_space: gym.spaces.Dict
671+
self.binary = binary
672+
if self.binary:
673+
self.observation_space.spaces.update(get_space(HandBinDictType).spaces)
674+
self.action_space.spaces.update(get_space(HandBinDictType).spaces)
675+
self.hand_key = get_space_keys(HandBinDictType)[0]
676+
else:
677+
self.observation_space.spaces.update(get_space(HandVecDictType).spaces)
678+
self.action_space.spaces.update(get_space(HandVecDictType).spaces)
679+
self.hand_key = get_space_keys(HandVecDictType)[0]
680+
self._hand = hand
681+
self._last_hand_cmd = None
682+
683+
def reset(self, **kwargs) -> tuple[dict[str, Any], dict[str, Any]]:
684+
self._hand.reset()
685+
self._last_hand_cmd = None
686+
return super().reset(**kwargs)
687+
688+
def observation(self, observation: dict[str, Any], info: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
689+
observation = copy.deepcopy(observation)
690+
if self.binary:
691+
observation[self.hand_key] = (
692+
self._last_hand_cmd if self._last_hand_cmd is not None else self.BINARY_HAND_OPEN
693+
)
694+
else:
695+
observation[self.hand_key] = self._hand.get_normalized_joints_poses()
696+
697+
info = {}
698+
return observation, info
699+
700+
def action(self, action: dict[str, Any]) -> dict[str, Any]:
701+
702+
action = copy.deepcopy(action)
703+
assert self.hand_key in action, "hand action not found."
704+
705+
hand_action = np.round(action[self.hand_key]) if self.binary else action[self.hand_key]
706+
hand_action = np.clip(hand_action, 0.0, 1.0)
707+
708+
if self.binary:
709+
if self._last_hand_cmd is None or self._last_hand_cmd != hand_action:
710+
if hand_action == self.BINARY_HAND_CLOSED:
711+
self._hand.grasp()
712+
else:
713+
self._hand.open()
714+
else:
715+
self._hand.set_normalized_joints_poses(hand_action)
716+
self._last_hand_cmd = hand_action
717+
del action[self.hand_key]
718+
return action

python/rcs/envs/creators.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import gymnasium as gym
66
import numpy as np
77
import rcs
8+
import rcs.hand.tilburg_hand
89
from gymnasium.envs.registration import EnvCreator
910
from rcs import sim
1011
from rcs._core.sim import CameraType
@@ -14,6 +15,7 @@
1415
CameraSetWrapper,
1516
ControlMode,
1617
GripperWrapper,
18+
HandWrapper,
1719
RelativeActionSpace,
1820
RelativeTo,
1921
RobotEnv,
@@ -37,6 +39,7 @@
3739
default_realsense,
3840
get_urdf_path,
3941
)
42+
from rcs.hand.tilburg_hand import TilburgHand
4043

4144
logger = logging.getLogger(__name__)
4245
logger.setLevel(logging.INFO)
@@ -53,7 +56,7 @@ def __call__( # type: ignore
5356
control_mode: ControlMode,
5457
robot_cfg: rcs.hw.FR3Config,
5558
collision_guard: str | PathLike | None = None,
56-
gripper_cfg: rcs.hw.FHConfig | None = None,
59+
gripper_cfg: rcs.hw.FHConfig | rcs.hand.tilburg_hand.THConfig | None = None,
5760
camera_set: BaseHardwareCameraSet | None = None,
5861
max_relative_movement: float | tuple[float, float] | None = None,
5962
relative_to: RelativeTo = RelativeTo.LAST_STEP,
@@ -89,9 +92,12 @@ def __call__( # type: ignore
8992
env: gym.Env = RobotEnv(robot, ControlMode.JOINTS if collision_guard is not None else control_mode)
9093

9194
env = FR3HW(env)
92-
if gripper_cfg is not None:
95+
if isinstance(gripper_cfg, rcs.hw.FHConfig):
9396
gripper = rcs.hw.FrankaHand(ip, gripper_cfg)
9497
env = GripperWrapper(env, gripper, binary=True)
98+
elif isinstance(gripper_cfg, rcs.hand.tilburg_hand.THConfig):
99+
hand = TilburgHand(gripper_cfg)
100+
env = HandWrapper(env, hand, binary=True)
95101

96102
if camera_set is not None:
97103
camera_set.start()
@@ -193,7 +199,7 @@ def __call__( # type: ignore
193199
camera_set = SimCameraSet(simulation, camera_set_cfg)
194200
env = CameraSetWrapper(env, camera_set, include_depth=True)
195201

196-
if gripper_cfg is not None:
202+
if gripper_cfg is not None and isinstance(gripper_cfg, rcs.sim.SimGripperConfig):
197203
gripper = sim.SimGripper(simulation, "0", gripper_cfg)
198204
env = GripperWrapper(env, gripper, binary=True)
199205
env = GripperWrapperSim(env, gripper)

python/rcs/envs/space_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Vec7Type: TypeAlias = np.ndarray[Literal[7], np.dtype[np.float64]]
2121
Vec3Type: TypeAlias = np.ndarray[Literal[3], np.dtype[np.float64]]
2222
Vec6Type: TypeAlias = np.ndarray[Literal[6], np.dtype[np.float64]]
23+
Vec18Type: TypeAlias = np.ndarray[Literal[18], np.dtype[np.float64]]
2324

2425

2526
class RCSpaceType(TypedDict): ...

python/rcs/envs/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rcs.camera.interface import BaseCameraConfig
1212
from rcs.camera.realsense import RealSenseCameraSet, RealSenseSetConfig
1313
from rcs.camera.sim import SimCameraConfig, SimCameraSetConfig
14+
from rcs.hand.tilburg_hand import THConfig
1415

1516
logger = logging.getLogger(__name__)
1617
logger.setLevel(logging.INFO)
@@ -41,6 +42,13 @@ def default_fr3_hw_gripper_cfg(async_control: bool = False):
4142
return gripper_cfg
4243

4344

45+
def default_tilburg_hw_hand_cfg(file: str | PathLike | None = None) -> THConfig:
46+
hand_cfg = THConfig()
47+
hand_cfg.grasp_percentage = 1.0
48+
hand_cfg.calibration_file = str(file) if isinstance(file, PathLike) else file
49+
return hand_cfg
50+
51+
4452
def default_realsense(name2id: dict[str, str] | None) -> RealSenseCameraSet | None:
4553
if name2id is None:
4654
return None

python/rcs/hand/__init__.py

Whitespace-only changes.

python/rcs/hand/interface.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import Protocol
2+
3+
import numpy as np
4+
5+
6+
class BaseHand(Protocol):
7+
"""
8+
Hand Class
9+
This class provides an interface for hand control.
10+
"""
11+
12+
def grasp(self):
13+
pass
14+
15+
def open(self):
16+
pass
17+
18+
def reset(self):
19+
pass
20+
21+
def close(self):
22+
pass
23+
24+
def get_state(self) -> np.ndarray:
25+
pass
26+
27+
def get_normalized_joints_poses(self) -> np.ndarray:
28+
pass
29+
30+
def set_normalized_joints_poses(self, values: np.ndarray):
31+
pass

0 commit comments

Comments
 (0)