@@ -124,15 +124,22 @@ def start(self) -> None:
124124 """Start the Quest teleoperation module."""
125125 super ().start ()
126126
127- subscriptions = [
128- (self .vr_left_pose , lambda msg : self ._on_pose_cb (Hand .LEFT , msg )),
129- (self .vr_right_pose , lambda msg : self ._on_pose_cb (Hand .RIGHT , msg )),
130- (self .vr_left_joy , lambda msg : self ._on_joy_cb (Hand .LEFT , msg )),
131- (self .vr_right_joy , lambda msg : self ._on_joy_cb (Hand .RIGHT , msg )),
132- ]
133- for stream , handler in subscriptions :
127+ input_streams = {
128+ "vr_left_pose" : (self .vr_left_pose , lambda msg : self ._on_pose (Hand .LEFT , msg )),
129+ "vr_right_pose" : (self .vr_right_pose , lambda msg : self ._on_pose (Hand .RIGHT , msg )),
130+ "vr_left_joy" : (self .vr_left_joy , lambda msg : self ._on_joy (Hand .LEFT , msg )),
131+ "vr_right_joy" : (self .vr_right_joy , lambda msg : self ._on_joy (Hand .RIGHT , msg )),
132+ }
133+ connected = []
134+ for name , (stream , handler ) in input_streams .items ():
134135 if stream and stream .transport : # type: ignore[attr-defined]
135136 self ._disposables .add (Disposable (stream .subscribe (handler ))) # type: ignore[attr-defined]
137+ connected .append (name )
138+
139+ if connected :
140+ logger .info (f"Subscribed to: { ', ' .join (connected )} " )
141+ else :
142+ logger .warning ("No input streams connected — module will not receive controller data" )
136143
137144 self ._start_control_loop ()
138145 logger .info ("Quest Teleoperation Module started" )
@@ -190,17 +197,17 @@ def get_status(self) -> QuestTeleopStatus:
190197 # Callbacks and Control Loop
191198 # -------------------------------------------------------------------------
192199
193- def _on_pose_cb (self , hand : Hand , pose_stamped : PoseStamped ) -> None :
200+ def _on_pose (self , hand : Hand , pose_stamped : PoseStamped ) -> None :
194201 """Callback for controller pose, converting WebXR to robot frame."""
195202 is_left = hand == Hand .LEFT
196203 robot_pose_stamped = webxr_to_robot (pose_stamped , is_left_controller = is_left )
197204 with self ._lock :
198205 self ._current_poses [hand ] = robot_pose_stamped
199206
200- def _on_joy_cb (self , hand : Hand , msg : Joy ) -> None :
207+ def _on_joy (self , hand : Hand , joy : Joy ) -> None :
201208 """Callback for Joy message, parsing into QuestControllerState."""
202209 is_left = hand == Hand .LEFT
203- controller = QuestControllerState .from_joy (msg , is_left = is_left )
210+ controller = QuestControllerState .from_joy (joy , is_left = is_left )
204211 with self ._lock :
205212 self ._controllers [hand ] = controller
206213
@@ -227,27 +234,32 @@ def _stop_control_loop(self) -> None:
227234 logger .info ("Control loop stopped" )
228235
229236 def _control_loop (self ) -> None :
230- """Main control loop: compute deltas and publish at fixed rate."""
237+ """Main control loop: compute deltas and publish at fixed rate.
238+
239+ Holds self._lock for the entire iteration so overridable methods
240+ don't need to acquire it themselves. Callbacks and @rpc methods
241+ still lock independently since they run on other threads.
242+ """
231243 period = 1.0 / self .config .control_loop_hz
232244
233245 while self ._control_loop_running :
234246 loop_start = time .perf_counter ()
235247 try :
236- self ._handle_engage ()
248+ with self ._lock :
249+ self ._handle_engage ()
237250
238- for hand in Hand :
239- if not self ._should_publish (hand ):
240- continue
241- output_pose = self ._get_output_pose (hand )
242- if output_pose is not None :
243- self ._publish_msg (hand , output_pose )
251+ for hand in Hand :
252+ if not self ._should_publish (hand ):
253+ continue
254+ output_pose = self ._get_output_pose (hand )
255+ if output_pose is not None :
256+ self ._publish_msg (hand , output_pose )
244257
245- # Always publish buttons regardless of engage state,
246- # so UI/listeners can react to button presses (e.g., trigger engage).
247- with self ._lock :
258+ # Always publish buttons regardless of engage state,
259+ # so UI/listeners can react to button presses (e.g., trigger engage).
248260 left = self ._controllers .get (Hand .LEFT )
249261 right = self ._controllers .get (Hand .RIGHT )
250- self ._publish_button_state (left , right )
262+ self ._publish_button_state (left , right )
251263 except Exception :
252264 logger .exception ("Error in teleop control loop" )
253265
@@ -257,7 +269,10 @@ def _control_loop(self) -> None:
257269 time .sleep (sleep_time )
258270
259271 # -------------------------------------------------------------------------
260- # Overridable Methods (for subclasses)
272+ # Control Loop Internals
273+ #
274+ # Called with self._lock held by the control loop.
275+ # Do NOT acquire self._lock in overrides.
261276 # -------------------------------------------------------------------------
262277
263278 def _handle_engage (self ) -> None :
@@ -266,26 +281,24 @@ def _handle_engage(self) -> None:
266281 Override to customize which button/action triggers engage.
267282 Default: Each controller's primary button (X/A) hold engages that hand.
268283 """
269- with self ._lock :
270- for hand in Hand :
271- controller = self ._controllers .get (hand )
272- if controller is None :
273- continue
274- if controller .primary :
275- if not self ._is_engaged [hand ]:
276- self .engage (hand )
277- else :
278- if self ._is_engaged [hand ]:
279- self .disengage (hand )
284+ for hand in Hand :
285+ controller = self ._controllers .get (hand )
286+ if controller is None :
287+ continue
288+ if controller .primary :
289+ if not self ._is_engaged [hand ]:
290+ self .engage (hand )
291+ else :
292+ if self ._is_engaged [hand ]:
293+ self .disengage (hand )
280294
281295 def _should_publish (self , hand : Hand ) -> bool :
282296 """Check if we should publish commands for a hand.
283297
284298 Override to add custom conditions.
285299 Default: Returns True if the hand is engaged.
286300 """
287- with self ._lock :
288- return self ._is_engaged [hand ]
301+ return self ._is_engaged [hand ]
289302
290303 def _get_output_pose (self , hand : Hand ) -> PoseStamped | None :
291304 """Get the pose to publish for a controller.
@@ -294,9 +307,8 @@ def _get_output_pose(self, hand: Hand) -> PoseStamped | None:
294307 apply scaling, add filtering).
295308 Default: Computes delta from initial pose.
296309 """
297- with self ._lock :
298- current_pose = self ._current_poses .get (hand )
299- initial_pose = self ._initial_poses .get (hand )
310+ current_pose = self ._current_poses .get (hand )
311+ initial_pose = self ._initial_poses .get (hand )
300312
301313 if current_pose is None or initial_pose is None :
302314 return None
0 commit comments