Skip to content

Commit 3b52565

Browse files
authored
Merge pull request #167 from utn-mi/juelg/osc
feat: async operational space controller - adds Operational Space Controller support for hardware RCS - adds async control support to RCS - started to remove app code from RCS such as vive teleop and recorder wrapper (we might want to readd them later again but first need to get them right, they currently have large typing issues)
2 parents b877f82 + 28a706a commit 3b52565

23 files changed

Lines changed: 1058 additions & 451 deletions

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ find_package(MuJoCo REQUIRED)
5353
FetchContent_Declare(
5454
libfranka
5555
GIT_REPOSITORY https://github.com/frankaemika/libfranka.git
56-
GIT_TAG 0.14.2
56+
GIT_TAG 0.15.0
5757
GIT_PROGRESS TRUE
5858
EXCLUDE_FROM_ALL
5959
)
@@ -98,7 +98,7 @@ FetchContent_MakeAvailable(libfranka rl pybind11)
9898
if (${INCLUDE_UTN_MODELS})
9999
if (DEFINED GITLAB_MODELS_TOKEN)
100100
include(download_scenes)
101-
set(ref bfdd70ae189ed3a925bf1c16d29162e847a21d56)
101+
set(ref 055a6fdab480dde8443030236012f76d9be90918)
102102
FetchContent_Declare(
103103
scenes
104104
URL "https://gitos.rrze.fau.de/api/v4/projects/1100/repository/archive?path=scenes&sha=${ref}"

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ dependencies = ["websockets>=11.0",
2222
"pillow~=10.3",
2323
"python-dotenv==1.0.1",
2424
"opencv-python~=4.10.0.84",
25+
"h5py~=3.13.0",
2526
# NOTE: when changing the mujoco version, also change it in requirements_dev.txt
2627
"mujoco==3.2.6",
2728
# NOTE: Same for pinocchio (pin)
28-
"pin==2.7.0"
29+
"pin==2.7.0",
2930
]
3031
readme = "README.md"
3132
maintainers = [

python/examples/fr3.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import rcsss
66
from rcsss import sim
7-
from rcsss._core.hw import FR3Config, IKController
7+
from rcsss._core.hw import FR3Config, IKSolver
88
from rcsss._core.sim import CameraType
99
from rcsss.camera.sim import SimCameraConfig, SimCameraSet, SimCameraSetConfig
1010
from rcsss.control.fr3_desk import FCI, Desk, DummyResourceManager
@@ -82,7 +82,7 @@ def main():
8282
# add camera to have a rendering gui
8383
cameras = {
8484
"default_free": SimCameraConfig(identifier="", type=int(CameraType.default_free)),
85-
"wrist": SimCameraConfig(identifier="eye-in-hand_0", type=int(CameraType.fixed)),
85+
"wrist": SimCameraConfig(identifier="wrist_0", type=int(CameraType.fixed)),
8686
}
8787
cam_cfg = SimCameraSetConfig(cameras=cameras, resolution_width=1280, resolution_height=720, frame_rate=20)
8888
camera_set = SimCameraSet(simulation, cam_cfg) # noqa: F841
@@ -95,7 +95,7 @@ def main():
9595
robot = rcsss.hw.FR3(ROBOT_IP, ik)
9696
robot_cfg = FR3Config()
9797
robot_cfg.tcp_offset = rcsss.common.Pose(rcsss.common.FrankaHandTCPOffset())
98-
robot_cfg.controller = IKController.robotics_library
98+
robot_cfg.ik_solver = IKSolver.rcs
9999
robot.set_parameters(robot_cfg)
100100

101101
gripper_cfg_hw = rcsss.hw.FHConfig()

python/examples/grasp_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def main():
9292
print(env.unwrapped.robot.get_cartesian_position().translation()) # type: ignore
9393
# assert False
9494
controller = PickUpDemo(env)
95-
controller.pickup("yellow_box_geom")
95+
controller.pickup("box_geom")
9696

9797

9898
if __name__ == "__main__":

python/examples/grasp_demo_lab.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def main():
8989
)
9090
env.reset()
9191
controller = PickUpDemo(env)
92-
controller.pickup("yellow_box_geom")
92+
controller.pickup("box_geom")
9393

9494

9595
if __name__ == "__main__":

python/rcsss/_core/hw/__init__.pyi

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ __all__ = [
1919
"FR3Load",
2020
"FR3State",
2121
"FrankaHand",
22-
"IKController",
22+
"IKSolver",
2323
"exceptions",
24-
"internal",
25-
"robotics_library",
24+
"franka",
25+
"rcs",
2626
]
2727

2828
class FHConfig(rcsss._core.common.GConfig):
29+
async_control: bool
2930
epsilon_inner: float
3031
epsilon_outer: float
3132
force: float
@@ -36,8 +37,12 @@ class FHConfig(rcsss._core.common.GConfig):
3637
class FHState(rcsss._core.common.GState):
3738
def __init__(self) -> None: ...
3839
@property
40+
def bool_state(self) -> bool: ...
41+
@property
3942
def is_grasped(self) -> bool: ...
4043
@property
44+
def is_moving(self) -> bool: ...
45+
@property
4146
def last_commanded_width(self) -> float: ...
4247
@property
4348
def max_unnormalized_width(self) -> float: ...
@@ -49,20 +54,35 @@ class FHState(rcsss._core.common.GState):
4954
class FR3(rcsss._core.common.Robot):
5055
def __init__(self, ip: str, ik: rcsss._core.common.IK | None = None) -> None: ...
5156
def automatic_error_recovery(self) -> None: ...
57+
def controller_set_joint_position(
58+
self, desired_q: numpy.ndarray[typing.Literal[7], numpy.dtype[numpy.float64]]
59+
) -> None: ...
5260
def double_tap_robot_to_continue(self) -> None: ...
5361
def get_parameters(self) -> FR3Config: ...
5462
def get_state(self) -> FR3State: ...
63+
def osc_set_cartesian_position(self, desired_pos_EE_in_base_frame: rcsss._core.common.Pose) -> None: ...
5564
def set_cartesian_position_ik(
5665
self, pose: rcsss._core.common.Pose, max_time: float, elbow: float | None, max_force: float | None = 5
5766
) -> None: ...
5867
def set_cartesian_position_internal(self, pose: rcsss._core.common.Pose) -> None: ...
5968
def set_default_robot_behavior(self) -> None: ...
60-
def set_guiding_mode(self, enabled: bool) -> None: ...
69+
def set_guiding_mode(
70+
self,
71+
x: bool = True,
72+
y: bool = True,
73+
z: bool = True,
74+
roll: bool = True,
75+
pitch: bool = True,
76+
yaw: bool = True,
77+
elbow: bool = True,
78+
) -> None: ...
6179
def set_parameters(self, cfg: FR3Config) -> bool: ...
80+
def stop_control_thread(self) -> None: ...
81+
def zero_torque_guiding(self) -> None: ...
6282

6383
class FR3Config(rcsss._core.common.RConfig):
64-
controller: IKController
65-
guiding_mode_enabled: bool
84+
async_control: bool
85+
ik_solver: IKSolver
6686
load_parameters: FR3Load | None
6787
nominal_end_effector_frame: rcsss._core.common.Pose | None
6888
speed_factor: float
@@ -87,20 +107,20 @@ class FrankaHand(rcsss._core.common.Gripper):
87107
def is_grasped(self) -> bool: ...
88108
def set_parameters(self, cfg: FHConfig) -> bool: ...
89109

90-
class IKController:
110+
class IKSolver:
91111
"""
92112
Members:
93113
94-
internal
114+
franka
95115
96-
robotics_library
116+
rcs
97117
"""
98118

99119
__members__: typing.ClassVar[
100-
dict[str, IKController]
101-
] # value = {'internal': <IKController.internal: 0>, 'robotics_library': <IKController.robotics_library: 1>}
102-
internal: typing.ClassVar[IKController] # value = <IKController.internal: 0>
103-
robotics_library: typing.ClassVar[IKController] # value = <IKController.robotics_library: 1>
120+
dict[str, IKSolver]
121+
] # value = {'franka': <IKSolver.franka: 0>, 'rcs': <IKSolver.rcs: 1>}
122+
franka: typing.ClassVar[IKSolver] # value = <IKSolver.franka: 0>
123+
rcs: typing.ClassVar[IKSolver] # value = <IKSolver.rcs: 1>
104124
def __eq__(self, other: typing.Any) -> bool: ...
105125
def __getstate__(self) -> int: ...
106126
def __hash__(self) -> int: ...
@@ -116,5 +136,5 @@ class IKController:
116136
@property
117137
def value(self) -> int: ...
118138

119-
internal: IKController # value = <IKController.internal: 0>
120-
robotics_library: IKController # value = <IKController.robotics_library: 1>
139+
franka: IKSolver # value = <IKSolver.franka: 0>
140+
rcs: IKSolver # value = <IKSolver.rcs: 1>

python/rcsss/camera/hw.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
BaseCameraSetConfig,
1515
Frame,
1616
FrameSet,
17+
SimpleFrameRate,
1718
)
1819

1920

@@ -40,6 +41,7 @@ def __init__(self):
4041
self._next_ring_index = 0
4142
self._buffer_len = 0
4243
self.writer: dict[str, cv2.VideoWriter] = {}
44+
self.rate = SimpleFrameRate()
4345

4446
def buffer_size(self) -> int:
4547
return len(self._buffer) - self._buffer.count(None)
@@ -91,33 +93,35 @@ def start(self, warm_up: bool = True):
9193
self._thread = threading.Thread(target=self.polling_thread, args=(warm_up,))
9294
self._thread.start()
9395

94-
def record_video(self, path: Path, episode: int):
96+
def record_video(self, path: Path, str_id: str):
97+
if self.recording_ongoing():
98+
return
99+
self.clear_buffer()
95100
for camera in self.camera_names:
96101
self.writer[camera] = cv2.VideoWriter(
97-
str(path / f"episode_{episode}_{camera}.mp4"),
102+
str(path / f"episode_{str_id}_{camera}.mp4"),
98103
# migh require to install ffmpeg
99104
cv2.VideoWriter_fourcc(*"mp4v"), # type: ignore
100105
self.config.frame_rate,
101106
(self.config.resolution_width, self.config.resolution_height),
102107
)
103108

109+
def recording_ongoing(self) -> bool:
110+
with self._buffer_lock:
111+
return len(self.writer) > 0
112+
104113
def stop_video(self):
105114
if len(self.writer) > 0:
106-
for camera_key, writer in self.writer.items():
107-
for i in range(self._next_ring_index):
108-
frameset = self._buffer[i]
109-
assert frameset is not None
110-
# rgb to bgr as expected by opencv
111-
writer.write(frameset.frames[camera_key].camera.color.data[:, :, ::-1])
112-
for camera in self.camera_names:
113-
self.writer[camera].release()
114-
self.writer = {}
115+
with self._buffer_lock:
116+
for camera in self.camera_names:
117+
self.writer[camera].release()
118+
self.writer = {}
115119

116120
def warm_up(self):
117121
for _ in range(self.config.warm_up_disposal_frames):
118122
for camera_name in self.camera_names:
119123
self._poll_frame(camera_name)
120-
sleep(1 / self.config.frame_rate)
124+
self.rate(self.config.frame_rate)
121125

122126
def polling_thread(self, warm_up: bool = True):
123127
if warm_up:
@@ -126,16 +130,14 @@ def polling_thread(self, warm_up: bool = True):
126130
frame_set = self.poll_frame_set()
127131
with self._buffer_lock:
128132
self._buffer[self._next_ring_index] = frame_set
133+
# copy the buffer to the record path
134+
for camera_key, writer in self.writer.items():
135+
frameset = self._buffer[self._next_ring_index]
136+
assert frameset is not None
137+
writer.write(frameset.frames[camera_key].camera.color.data[:, :, ::-1])
129138
self._next_ring_index = (self._next_ring_index + 1) % self.config.max_buffer_frames
130139
self._buffer_len = max(self._buffer_len + 1, self.config.max_buffer_frames)
131-
if self._next_ring_index == 0:
132-
# copy the buffer to the record path
133-
for camera_key, writer in self.writer.items():
134-
for i in range(self.config.max_buffer_frames):
135-
frameset = self._buffer[i]
136-
assert frameset is not None
137-
writer.write(frameset.frames[camera_key].camera.color.data[:, :, ::-1])
138-
sleep(1 / self.config.frame_rate)
140+
self.rate(self.config.frame_rate)
139141

140142
def poll_frame_set(self) -> FrameSet:
141143
"""Gather frames over all available cameras."""

python/rcsss/camera/interface.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
1+
import logging
12
from dataclasses import dataclass
23
from datetime import datetime
4+
from time import sleep, time
35
from typing import Any, Protocol
46

57
from pydantic import BaseModel, Field
68

9+
logger = logging.getLogger(__name__)
10+
logger.setLevel(logging.INFO)
11+
12+
13+
class SimpleFrameRate:
14+
def __init__(self):
15+
self.t = None
16+
self._last_print = None
17+
18+
def reset(self):
19+
self.t = None
20+
21+
def __call__(self, frame_rate: int | float):
22+
if self.t is None:
23+
self.t = time()
24+
self._last_print = self.t
25+
sleep(1 / frame_rate if isinstance(frame_rate, int) else frame_rate)
26+
return
27+
sleep_time = (
28+
1 / frame_rate - (time() - self.t) if isinstance(frame_rate, int) else frame_rate - (time() - self.t)
29+
)
30+
if sleep_time > 0:
31+
sleep(sleep_time)
32+
if self._last_print is None or time() - self._last_print > 10:
33+
self._last_print = time()
34+
logger.info(f"FPS: {1 / (time() - self.t)}")
35+
36+
self.t = time()
37+
738

839
class BaseCameraConfig(BaseModel):
940
identifier: str

python/rcsss/cli/main.py

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import logging
22
from contextlib import ExitStack
3-
from pathlib import Path
43
from time import sleep
54
from typing import Annotated, Optional
65

76
import pyrealsense2 as rs
87
import rcsss
98
import rcsss.control.fr3_desk
109
import typer
11-
from PIL import Image
1210
from rcsss.camera.realsense import RealSenseCameraSet
13-
from rcsss.config import create_sample_config_yaml, read_config_yaml
1411
from rcsss.control.record import PoseList
1512
from rcsss.control.utils import load_creds_fr3_desk
1613
from rcsss.envs.factories import get_urdf_path
@@ -21,16 +18,6 @@
2118
main_app = typer.Typer(help="CLI tool for the Robot Control Stack (RCS).")
2219

2320

24-
@main_app.command()
25-
def sample_config(
26-
path: Annotated[
27-
str, typer.Option("-p", help="Path to where the default config file should be saved")
28-
] = "config.yaml",
29-
):
30-
"""Creates a sample yaml config file"""
31-
create_sample_config_yaml(path)
32-
33-
3421
# REALSENSE CLI
3522
realsense_app = typer.Typer()
3623
main_app.add_typer(
@@ -53,52 +40,6 @@ def serials():
5340
logger.info(" %s: %s", device.product_line, device.serial)
5441

5542

56-
@realsense_app.command()
57-
def test(
58-
path: Annotated[str, typer.Argument(help="Path to the config file")],
59-
):
60-
"""Tests all configured and connected realsense by saving the current picture."""
61-
cfg = read_config_yaml(path)
62-
assert cfg.hw.camera_type == "realsense", "Only realsense cameras are supported for this test."
63-
assert cfg.hw.camera_config is not None, "Camera config is not set."
64-
assert cfg.hw.camera_config.realsense_config is not None, "Realsense config is not set."
65-
cs = RealSenseCameraSet(cfg.hw.camera_config.realsense_config)
66-
cs.warm_up()
67-
testdir = Path(cfg.hw.camera_config.realsense_config.record_path)
68-
frame_set = cs.poll_frame_set()
69-
for device_str in cfg.hw.camera_config.realsense_config.name_to_identifier:
70-
assert device_str in frame_set.frames
71-
frame = frame_set.frames[device_str]
72-
if frame.camera.color is not None:
73-
im = Image.fromarray(frame.camera.color.data, mode="RGB")
74-
im.save(testdir / f"test_img_{device_str}.png")
75-
if frame.camera.depth is not None:
76-
im = Image.fromarray(frame.camera.depth.data)
77-
im.save(testdir / f"test_depth_{device_str}.png")
78-
if frame.camera.ir is not None:
79-
im = Image.fromarray(frame.camera.ir.data)
80-
im.save(testdir / f"test_ir_{device_str}.png")
81-
if frame.imu is not None:
82-
logger.info("IMU data: %s %s", frame.imu.accel, frame.imu.gyro)
83-
84-
85-
@realsense_app.command()
86-
def test_record(
87-
path: Annotated[str, typer.Argument(help="Path to the config file")],
88-
n_frames: Annotated[int, typer.Argument(help="Name of the camera that should be tested")] = 100,
89-
):
90-
"""Tests all configured and connected realsense by saving the current picture."""
91-
cfg = read_config_yaml(path)
92-
assert cfg.hw.camera_type == "realsense", "Only realsense cameras are supported for this test."
93-
assert cfg.hw.camera_config is not None, "Camera config is not set."
94-
assert cfg.hw.camera_config.realsense_config is not None, "Realsense config is not set."
95-
cs = RealSenseCameraSet(cfg.hw.camera_config.realsense_config)
96-
cs.start(warm_up=True)
97-
while cs.buffer_size() < n_frames:
98-
sleep(1 / cfg.hw.camera_config.realsense_config.frame_rate)
99-
cs.stop()
100-
101-
10243
# FR3 CLI
10344
fr3_app = typer.Typer()
10445
main_app.add_typer(

0 commit comments

Comments
 (0)