Skip to content

Commit 6203369

Browse files
authored
Merge pull request #188 from utn-mi/gamal/digit_cam
feat(digit_cam): Add DIGIT tactile sensor support to RCS
2 parents 2c55716 + 5e2367e commit 6203369

File tree

6 files changed

+241
-0
lines changed

6 files changed

+241
-0
lines changed

pyproject.toml

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

python/rcs/camera/digit_cam.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from digit_interface.digit import Digit
2+
from rcs.camera.hw import BaseHardwareCameraSet, HWCameraSetConfig
3+
from rcs.camera.interface import CameraFrame, DataFrame, Frame
4+
5+
6+
class DigitConfig(HWCameraSetConfig):
7+
"""
8+
Configuration for the DIGIT device.
9+
This class is used to define the settings for the DIGIT device.
10+
"""
11+
12+
stream_name: str = "QVGA" # options: "QVGA" (60 and 30 fps), "VGA" (30 and 15 fps)
13+
14+
15+
class DigitCam(BaseHardwareCameraSet):
16+
"""
17+
This module provides an interface to interact with the DIGIT device.
18+
It allows for connecting to the device, changing settings, and retrieving information.
19+
"""
20+
21+
def __init__(self, cfg: DigitConfig):
22+
self._cfg = cfg
23+
super().__init__()
24+
self._cameras: dict[str, Digit] = {}
25+
self.initalize(self.config)
26+
27+
def initalize(self, cfg: HWCameraSetConfig):
28+
"""
29+
Initialize the digit interface with the given configuration.
30+
:param cfg: Configuration for the DIGIT device.
31+
"""
32+
for name, serial in cfg.name_to_identifier.items():
33+
digit = Digit(serial, name)
34+
digit.connect()
35+
self._cameras[name] = digit
36+
37+
def _poll_frame(self, camera_name: str) -> Frame:
38+
"""Polls the frame from the camera with the given name."""
39+
digit = self._cameras[camera_name]
40+
frame = digit.get_frame()
41+
color = DataFrame(data=frame)
42+
# rgb to bgr as expected by opencv
43+
# color = DataFrame(data=frame[:, :, ::-1])
44+
cf = CameraFrame(color=color)
45+
46+
return Frame(camera=cf)
47+
48+
@property
49+
def config(self) -> DigitConfig:
50+
return self._cfg

python/rcs/envs/base.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,23 @@ class CameraDictType(RCSpaceType):
137137
]
138138

139139

140+
class DigitCameraDictType(RCSpaceType):
141+
digit_frames: dict[
142+
Annotated[str, "camera_names"],
143+
Annotated[
144+
np.ndarray,
145+
# needs to be filled with values downstream
146+
lambda height, width, color_dim=3, dtype=np.uint8, low=0, high=255: gym.spaces.Box(
147+
low=low,
148+
high=high,
149+
shape=(height, width, color_dim),
150+
dtype=dtype,
151+
),
152+
"digit_frames",
153+
],
154+
]
155+
156+
140157
# joining works with inheritance but need to inherit from protocol again
141158
class ArmObsType(TQuatDictType, JointsDictType, TRPYDictType): ...
142159

@@ -646,6 +663,54 @@ def close(self):
646663
super().close()
647664

648665

666+
class DigitCameraSetWrapper(CameraSetWrapper):
667+
"""Wrapper for digit cameras."""
668+
669+
def __init__(self, env, camera_set: BaseCameraSet):
670+
super().__init__(env, camera_set)
671+
# self.unwrapped: FR3Env
672+
self.camera_set = camera_set
673+
self.observation_space: gym.spaces.Dict
674+
# rgb is always included
675+
params: dict = {
676+
"digit_frames": {
677+
"height": camera_set.config.resolution_height,
678+
"width": camera_set.config.resolution_width,
679+
}
680+
}
681+
682+
self.observation_space.spaces.update(
683+
get_space(
684+
DigitCameraDictType,
685+
child_dict_keys_to_unfold={
686+
"camera_names": camera_set.camera_names,
687+
},
688+
params=params,
689+
).spaces
690+
)
691+
self.camera_key = get_space_keys(DigitCameraDictType)[0]
692+
693+
def observation(self, observation: dict, info: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
694+
observation = copy.deepcopy(observation)
695+
info = copy.deepcopy(info)
696+
frameset = self.camera_set.get_latest_frames()
697+
if frameset is None:
698+
observation[self.camera_key] = {}
699+
info["digit_available"] = False
700+
return observation, info
701+
702+
frame_dict: dict[str, np.ndarray] = {
703+
camera_name: frame.camera.color.data for camera_name, frame in frameset.frames.items()
704+
}
705+
observation[self.camera_key] = frame_dict
706+
707+
info["digit_available"] = True
708+
if frameset.avg_timestamp is not None:
709+
info["digit_timestamp"] = frameset.avg_timestamp
710+
711+
return observation, info
712+
713+
649714
class GripperWrapper(ActObsInfoWrapper):
650715
# TODO: sticky gripper, like in aloha
651716

python/rcs/envs/creators.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from gymnasium.envs.registration import EnvCreator
1010
from rcs import sim
1111
from rcs._core.sim import CameraType
12+
from rcs.camera.digit_cam import DigitCam
1213
from rcs.camera.hw import BaseHardwareCameraSet
1314
from rcs.camera.sim import SimCameraConfig, SimCameraSet, SimCameraSetConfig
1415
from rcs.envs.base import (
1516
CameraSetWrapper,
1617
ControlMode,
18+
DigitCameraSetWrapper,
1719
GripperWrapper,
1820
HandWrapper,
1921
MultiRobotWrapper,
@@ -59,6 +61,7 @@ def __call__( # type: ignore
5961
collision_guard: str | PathLike | None = None,
6062
gripper_cfg: rcs.hw.FHConfig | rcs.hand.tilburg_hand.THConfig | None = None,
6163
camera_set: BaseHardwareCameraSet | None = None,
64+
digit_set: DigitCam | None = None,
6265
max_relative_movement: float | tuple[float, float] | None = None,
6366
relative_to: RelativeTo = RelativeTo.LAST_STEP,
6467
urdf_path: str | PathLike | None = None,
@@ -106,6 +109,12 @@ def __call__( # type: ignore
106109
logger.info("CameraSet started")
107110
env = CameraSetWrapper(env, camera_set)
108111

112+
if digit_set is not None:
113+
digit_set.start()
114+
digit_set.wait_for_frames()
115+
logger.info("DigitCameraSet started")
116+
env = DigitCameraSetWrapper(env, digit_set)
117+
109118
if collision_guard is not None:
110119
assert urdf_path is not None
111120
env = CollisionGuard.env_from_xml_paths(

python/rcs/envs/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import mujoco as mj
66
import numpy as np
77
import rcs
8+
from digit_interface import Digit
89
from rcs import sim
910
from rcs._core.hw import FR3Config, IKSolver
1011
from rcs._core.sim import CameraType
12+
from rcs.camera.digit_cam import DigitCam, DigitConfig
1113
from rcs.camera.interface import BaseCameraConfig
1214
from rcs.camera.realsense import RealSenseCameraSet, RealSenseSetConfig
1315
from rcs.camera.sim import SimCameraConfig, SimCameraSetConfig
@@ -73,6 +75,21 @@ def default_fr3_sim_gripper_cfg(idx: str = "0") -> sim.SimGripperConfig:
7375
return cfg
7476

7577

78+
def default_digit(name2id: dict[str, str] | None, stream_name: str = "QVGA") -> DigitCam | None:
79+
if name2id is None:
80+
return None
81+
cameras = {name: BaseCameraConfig(identifier=id) for name, id in name2id.items()}
82+
stream_dict = Digit.STREAMS[stream_name]
83+
digit_cfg = DigitConfig(
84+
cameras=cameras,
85+
resolution_height=stream_dict["resolution"]["height"],
86+
resolution_width=stream_dict["resolution"]["width"],
87+
stream_name=stream_name,
88+
frame_rate=stream_dict["fps"]["30fps"],
89+
)
90+
return DigitCam(digit_cfg)
91+
92+
7693
def default_mujoco_cameraset_cfg() -> SimCameraSetConfig:
7794
cameras = {
7895
"wrist": SimCameraConfig(identifier="wrist_0", type=int(CameraType.fixed)),

0 commit comments

Comments
 (0)