Skip to content

Commit b4ef47e

Browse files
committed
refactor(operator): key translation and origin reset
- refactored origin reset - proper operator interface - key translation
1 parent 1119c07 commit b4ef47e

2 files changed

Lines changed: 130 additions & 139 deletions

File tree

‎python/rcs/operator/interface.py‎

Lines changed: 78 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,135 @@
11
from abc import ABC
2-
from dataclasses import dataclass
3-
from enum import Enum, auto
4-
import threading
52
import copy
3+
from dataclasses import dataclass, field
4+
import threading
65
from time import sleep
7-
from typing import Protocol
8-
import numpy as np
96
import gymnasium as gym
107

118
from rcs.envs.base import ArmWithGripper, ControlMode, RelativeTo
9+
from rcs.sim.sim import Sim
1210
from rcs.utils import SimpleFrameRate
1311

12+
1413
@dataclass
1514
class TeleopCommands:
1615
"""Semantic commands decoupled from specific hardware buttons."""
16+
1717
record: bool = False
1818
success: bool = False
1919
failure: bool = False
20-
reset_origin: bool = False
20+
reset_origin_to_current: dict[str, bool] = field(default_factory=dict)
21+
2122

2223
@dataclass(kw_only=True)
2324
class BaseOperatorConfig:
24-
env_frequency: int = 30
25+
read_frequency: int = 30
26+
simulation: bool = True
27+
2528

2629
class BaseOperator(ABC, threading.Thread):
27-
"""Interface for an operator device"""
28-
29-
# Define this as a class attribute so it can be accessed without instantiating
3030
control_mode: tuple[ControlMode, RelativeTo]
31+
controller_names: list[str] = field(default=["left", "right"])
3132

32-
def __init__(self, env: gym.Env, config: BaseOperatorConfig):
33-
super().__init__()
33+
def __init__(self, config: BaseOperatorConfig, sim: Sim | None = None):
3434
self.config = config
35-
self.env = env
36-
self.reset_lock = threading.Lock()
37-
self._exit_requested = False
38-
39-
# State for semantic commands
40-
self._commands = TeleopCommands()
41-
self._cmd_lock = threading.Lock()
35+
self.sim = sim
4236

4337
def consume_commands(self) -> TeleopCommands:
44-
"""Returns the current commands and resets them to False (edge-triggered)."""
45-
with self._cmd_lock:
46-
cmds = copy.copy(self._commands)
47-
self._commands.record = False
48-
self._commands.success = False
49-
self._commands.failure = False
50-
self._commands.reset_origin = False
51-
return cmds
38+
"""Returns the current commands and resets them (edge-triggered). Must be thread-safe."""
39+
raise NotImplementedError()
5240

5341
def reset_operator_state(self):
54-
"""Hook for subclasses to reset their internal poses/offsets on env reset."""
55-
pass
42+
"""Hook for subclasses to reset their internal poses/offsets on env reset. Must be thread-safe."""
5643

5744
def run(self):
5845
"""Read out hardware, set states and process buttons."""
5946
raise NotImplementedError()
6047

61-
# TODO: support multiple robots
62-
def get_action(self) -> dict[str, ArmWithGripper]:
63-
"""Returns the action dictionary to step the environment."""
48+
def consume_action(self) -> dict[str, ArmWithGripper]:
49+
"""Returns the action dictionary to step the environment. Must be thread-safe."""
6450
raise NotImplementedError()
6551

52+
53+
class TeleopLoop:
54+
"""Interface for an operator device"""
55+
56+
# Define this as a class attribute so it can be accessed without instantiating
57+
control_mode: tuple[ControlMode, RelativeTo]
58+
59+
def __init__(
60+
self,
61+
env: gym.Env,
62+
operator: BaseOperator,
63+
env_frequency: int = 30,
64+
key_translation: dict[str, str] | None = None,
65+
):
66+
super().__init__()
67+
self.env = env
68+
self.operator = operator
69+
self._exit_requested = False
70+
self.env_frequency = env_frequency
71+
if key_translation is None:
72+
# controller to robot translation
73+
self.key_translation = {key: key for key in self.operator.controller_names}
74+
else:
75+
self.key_translation = key_translation
76+
6677
def stop(self):
6778
self._exit_requested = True
68-
self.join()
79+
self.operator.join()
6980

7081
def __enter__(self):
71-
self.start()
82+
self.operator.start()
7283
return self
7384

7485
def __exit__(self, *_):
7586
self.stop()
7687

88+
def _translate_keys(self, actions):
89+
return {self.key_translation[key]: actions[key] for key in actions}
90+
7791
def environment_step_loop(self):
78-
rate_limiter = SimpleFrameRate(self.config.env_frequency, "env loop")
92+
rate_limiter = SimpleFrameRate(self.env_frequency, "env loop")
7993
while True:
8094
if self._exit_requested:
8195
break
82-
96+
8397
# 1. Process Meta-Commands
84-
cmds = self.consume_commands()
85-
98+
cmds = self.operator.consume_commands()
99+
86100
if cmds.record:
87101
print("Command: Start Recording")
88102
self.env.get_wrapper_attr("start_record")()
89-
103+
90104
if cmds.success:
91105
print("Command: Success! Resetting env...")
92-
with self.reset_lock:
93-
self.env.get_wrapper_attr("success")()
94-
sleep(1) # sleep to let the robot reach the goal
95-
self.env.reset()
96-
self.reset_operator_state()
97-
106+
self.env.get_wrapper_attr("success")()
107+
sleep(1) # sleep to let the robot reach the goal
108+
self.env.reset()
109+
self.operator.reset_operator_state()
110+
# consume new commands because of potential origin reset
111+
continue
112+
98113
elif cmds.failure:
99114
print("Command: Failure! Resetting env...")
100-
with self.reset_lock:
101-
self.env.reset()
102-
self.reset_operator_state()
103-
104-
# if cmds.reset_origin:
105-
# print("Command: Resetting origin...")
106-
# # env lock
107-
# for robot in self.config.robot_keys:
108-
# self.env.envs[robot].set_origin_to_current()
109-
115+
self.env.reset()
116+
self.operator.reset_operator_state()
117+
# consume new commands because of potential origin reset
118+
continue
119+
120+
for controller in cmds.reset_origin_to_current:
121+
if cmds.reset_origin_to_current[controller]:
122+
robot = self.key_translation[controller]
123+
print(f"Command: Resetting origin for {robot}...")
124+
assert (
125+
self.operator.control_mode[1] == RelativeTo.CONFIGURED_ORIGIN
126+
and self.env.get_wrapper_attr("relative_to") == RelativeTo.CONFIGURED_ORIGIN
127+
), "both robot env and operator must be configured to relative_to.CONFIGURED_ORIGIN"
128+
self.env.get_wrapper_attr("envs")[robot].set_origin_to_current()
110129

111130
# 2. Step the Environment
112-
with self.reset_lock:
113-
actions = self.get_action()
114-
if actions: # Only step if actions are provided
115-
self.env.step(actions)
116-
117-
rate_limiter()
118-
119-
120-
class CompositeOperator(BaseOperator):
121-
def __init__(self, env, motion_operator: BaseOperator, command_operator: BaseOperator):
122-
# We don't need a specific config for the composite itself,
123-
# so we just pass a default one to the base class
124-
super().__init__(env, BaseOperatorConfig())
125-
126-
self.motion_op = motion_operator
127-
self.command_op = command_operator
128-
129-
# Inherit the control mode from the motion operator (e.g., GELLO)
130-
self.control_mode = self.motion_op.control_mode
131+
actions = self.operator.consume_action()
132+
actions = self._translate_keys(actions)
133+
self.env.step(actions)
131134

132-
def start(self):
133-
"""Start the background threads for both hardware readers."""
134-
self.motion_op.start()
135-
self.command_op.start()
136-
137-
def stop(self):
138-
"""Stop both hardware readers."""
139-
self.motion_op.stop()
140-
self.command_op.stop()
141-
self._exit_requested = True
142-
143-
def get_action(self):
144-
"""Fetch the physical movements from the motion operator (GELLO)."""
145-
return self.motion_op.get_action()
146-
147-
def consume_commands(self) -> TeleopCommands:
148-
"""Fetch the meta-commands (record/success/fail) from the command operator (Pedal)."""
149-
# If both devices can send commands, you could logically OR them together here.
150-
# But in this case, only the pedal sends commands.
151-
return self.command_op.consume_commands()
152-
153-
def reset_operator_state(self):
154-
"""Pass the reset hook down to the operators."""
155-
self.motion_op.reset_operator_state()
156-
self.command_op.reset_operator_state()
157-
158-
def run(self):
159-
# The base class requires this, but the sub-operators handle their own run loops.
160-
pass
135+
rate_limiter()

0 commit comments

Comments
 (0)