11from abc import ABC
2- from dataclasses import dataclass
3- from enum import Enum , auto
4- import threading
52import copy
3+ from dataclasses import dataclass , field
4+ import threading
65from time import sleep
7- from typing import Protocol
8- import numpy as np
96import gymnasium as gym
107
118from rcs .envs .base import ArmWithGripper , ControlMode , RelativeTo
9+ from rcs .sim .sim import Sim
1210from rcs .utils import SimpleFrameRate
1311
12+
1413@dataclass
1514class 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 )
2324class BaseOperatorConfig :
24- env_frequency : int = 30
25+ read_frequency : int = 30
26+ simulation : bool = True
27+
2528
2629class 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