Skip to content

Commit a879860

Browse files
authored
Merge pull request #212 from RobotControlStack/7-remote-robot-control-through-rpc-wrappers
feat(envs): remote robot control through rpc wrappers
2 parents 14e2a30 + 0d2d54e commit a879860

File tree

7 files changed

+407
-1
lines changed

7 files changed

+407
-1
lines changed

README.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,69 @@ for _ in range(100):
120120
print(obs)
121121
if truncated or terminated:
122122
logger.info("Truncated or terminated!")
123-
return
123+
exit()
124124
```
125+
126+
### Remote Procedure Call (RPC) Client and Server
127+
#### Server
128+
```python
129+
from rcs.envs.creators import SimEnvCreator
130+
from rcs.envs.utils import (
131+
default_mujoco_cameraset_cfg,
132+
default_sim_gripper_cfg,
133+
default_sim_robot_cfg,
134+
)
135+
from rcs.envs.base import ControlMode, RelativeTo
136+
from rcs.rpc.server import RcsServer
137+
138+
def run_server():
139+
env = SimEnvCreator()(
140+
control_mode=ControlMode.JOINTS,
141+
collision_guard=False,
142+
robot_cfg=default_sim_robot_cfg(),
143+
gripper_cfg=default_sim_gripper_cfg(),
144+
cameras=default_mujoco_cameraset_cfg(),
145+
max_relative_movement=0.1,
146+
relative_to=RelativeTo.LAST_STEP,
147+
)
148+
server = RcsServer(env, port=50051)
149+
server.start()
150+
151+
if __name__ == "__main__":
152+
run_server()
153+
```
154+
155+
#### Client
156+
```python
157+
import time
158+
from python.rcs.rpc.client import RcsClient
159+
160+
if __name__ == "__main__":
161+
# Create the client (adjust host/port if needed)
162+
client = RcsClient(host="localhost", port=50051)
163+
164+
try:
165+
print("Resetting environment...")
166+
obs = client.reset()
167+
print(f"Initial observation: {obs}")
168+
169+
for i in range(5):
170+
print(f"\nStep {i+1}")
171+
# Replace with a valid action for your environment
172+
action = 0
173+
obs, reward, terminated, truncated, info = client.step(action)
174+
print(f"Obs: {obs}, Reward: {reward}, Terminated: {terminated}, Truncated: {truncated}, Info: {info}")
175+
if terminated or truncated:
176+
print("Episode finished, resetting...")
177+
obs = client.reset()
178+
print(f"Reset observation: {obs}")
179+
time.sleep(0.5)
180+
finally:
181+
print("Closing client.")
182+
client.close()
183+
```
184+
185+
125186
### Examples
126187
Checkout the python examples in the [examples](examples) folder:
127188
- [fr3_direct_control.py](examples/fr3.py) shows direct robot control with RCS's python bindings

examples/rpc_run_client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import time
2+
3+
from python.rcs.rpc.client import RcsClient
4+
5+
if __name__ == "__main__":
6+
# Create the client (adjust host/port if needed)
7+
client = RcsClient(host="localhost", port=50051)
8+
9+
try:
10+
print("Resetting environment...")
11+
obs = client.reset()
12+
print(f"Initial observation: {obs}")
13+
14+
for i in range(5):
15+
print(f"\nStep {i+1}")
16+
# Replace with a valid action for your environment
17+
action = 0
18+
obs, reward, terminated, truncated, info = client.step(action)
19+
print(f"Obs: {obs}, Reward: {reward}, Terminated: {terminated}, Truncated: {truncated}, Info: {info}")
20+
if terminated or truncated:
21+
print("Episode finished, resetting...")
22+
obs = client.reset()
23+
print(f"Reset observation: {obs}")
24+
time.sleep(0.5)
25+
finally:
26+
print("Closing client.")
27+
client.close()

examples/rpc_run_server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from rcs.envs.base import ControlMode, RelativeTo
2+
from rcs.envs.creators import SimEnvCreator
3+
from rcs.envs.utils import (
4+
default_mujoco_cameraset_cfg,
5+
default_sim_gripper_cfg,
6+
default_sim_robot_cfg,
7+
)
8+
from rcs.rpc.server import RcsServer
9+
10+
11+
def run_server():
12+
env = SimEnvCreator()(
13+
control_mode=ControlMode.JOINTS,
14+
collision_guard=False,
15+
robot_cfg=default_sim_robot_cfg(),
16+
gripper_cfg=default_sim_gripper_cfg(),
17+
cameras=default_mujoco_cameraset_cfg(),
18+
max_relative_movement=0.1,
19+
relative_to=RelativeTo.LAST_STEP,
20+
)
21+
server = RcsServer(env, port=50051)
22+
server.start()
23+
24+
25+
if __name__ == "__main__":
26+
run_server()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies = ["websockets>=11.0",
2626
"tilburg-hand",
2727
"digit-interface",
2828
"ompl>=1.7.0",
29+
"rpyc==6.0.2",
2930
]
3031
readme = "README.md"
3132
maintainers = [

python/rcs/rpc/client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import gymnasium as gym
2+
import rpyc
3+
from rpyc.utils.classic import obtain
4+
5+
6+
class RcsClient(gym.Env):
7+
def __init__(self, host="localhost", port=50051):
8+
super().__init__()
9+
self.conn = rpyc.connect(host, port)
10+
self.server = self.conn.root
11+
# Optionally, fetch spaces from server if needed
12+
# self.observation_space = ...
13+
# self.action_space = ...
14+
15+
def step(self, action):
16+
return self.server.step(action)
17+
18+
def reset(self, **kwargs):
19+
return self.server.reset(**kwargs)
20+
21+
def get_obs(self):
22+
return self.server.get_obs()
23+
24+
@property
25+
def unwrapped(self):
26+
return self.server.unwrapped()
27+
28+
@property
29+
def action_space(self):
30+
return obtain(self.server.action_space())
31+
32+
def close(self):
33+
self.conn.close()

python/rcs/rpc/server.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# import wrapper
2+
import rpyc
3+
from gymnasium import Wrapper
4+
from rpyc.utils.server import ThreadedServer
5+
6+
rpyc.core.protocol.DEFAULT_CONFIG["allow_pickle"] = True
7+
8+
9+
@rpyc.service
10+
class RcsServer(Wrapper, rpyc.Service):
11+
def __init__(self, env, host="localhost", port=50051):
12+
super().__init__(env)
13+
self.host = host
14+
self.port = port
15+
16+
@rpyc.exposed
17+
def step(self, action):
18+
"""Perform a step in the environment using the Wrapper base class."""
19+
return super().step(action)
20+
21+
@rpyc.exposed
22+
def reset(self, **kwargs):
23+
"""Reset the environment using the Wrapper base class."""
24+
return super().reset(**kwargs)
25+
26+
@rpyc.exposed
27+
def get_obs(self):
28+
"""Get the current observation using the Wrapper base class if available."""
29+
if hasattr(super(), "get_obs"):
30+
return super().get_obs()
31+
if hasattr(self.env, "get_obs"):
32+
return self.env.get_obs()
33+
error = "The environment does not have a get_obs method."
34+
raise NotImplementedError(error)
35+
36+
@rpyc.exposed
37+
def unwrapped(self):
38+
"""Return the unwrapped environment using the Wrapper base class."""
39+
return super().unwrapped
40+
41+
@rpyc.exposed
42+
def action_space(self):
43+
"""Return the action space using the Wrapper base class."""
44+
return super().action_space
45+
46+
def start(self):
47+
print(f"Starting RcsServer RPC (looped OneShotServer) on {self.host}:{self.port}")
48+
t = ThreadedServer(self, port=self.port)
49+
t.start()

0 commit comments

Comments
 (0)