2727from __future__ import annotations
2828
2929import logging
30- import threading
31- import time
3230from typing import TYPE_CHECKING
3331
3432import mujoco
@@ -193,10 +191,6 @@ class TeleopPanel(PanelBase):
193191 arm_label: Display label (e.g., "Right Arm").
194192 """
195193
196- # Shared lock across all TeleopPanel instances — prevents concurrent
197- # MuJoCo access when multiple arms are teleoped simultaneously.
198- _sim_lock = threading .Lock ()
199-
200194 def __init__ (
201195 self ,
202196 arm : Arm ,
@@ -207,6 +201,7 @@ def __init__(
207201 arm_label : str = "Arm" ,
208202 abort_fn : object | None = None ,
209203 clear_abort_fn : object | None = None ,
204+ event_loop : object | None = None ,
210205 ):
211206 self ._arm = arm
212207 self ._controller = controller
@@ -216,6 +211,7 @@ def __init__(
216211 self ._arm_label = arm_label
217212 self ._abort_fn = abort_fn # callable → bool, checked in teleop loop
218213 self ._clear_abort_fn = clear_abort_fn # callable → None, clears abort
214+ self ._event_loop = event_loop # PhysicsEventLoop — steps teleop from main thread
219215 self ._gizmo = None
220216 self ._ghost = None
221217 self ._is_teleop_active = False
@@ -337,15 +333,15 @@ def _activate_teleop(self) -> None:
337333 self ._activate_btn .name = "Deactivate"
338334 self ._activate_btn .color = "red"
339335
340- # Start background loop that steps the controller + syncs viewer
341- self ._teleop_thread = threading .Thread (
342- target = self ._teleop_loop ,
343- daemon = True ,
344- )
345- self ._teleop_thread .start ()
336+ # Register with event loop — teleop stepping happens on the main
337+ # thread via inputhook tick(), not in a background thread.
338+ if self ._event_loop is not None :
339+ self ._event_loop .register_teleop (self ._controller , self )
346340
347341 def _deactivate_teleop (self ) -> None :
348- self ._is_teleop_active = False # signals thread to stop
342+ self ._is_teleop_active = False
343+ if self ._event_loop is not None :
344+ self ._event_loop .unregister_teleop (self ._controller )
349345 self ._controller .deactivate ()
350346
351347 self ._gizmo .visible = False
@@ -356,40 +352,21 @@ def _deactivate_teleop(self) -> None:
356352 self ._activate_btn .color = "green"
357353 self ._status_md .content = "⚪ **Idle**"
358354
359- def _teleop_loop (self ) -> None :
360- """Background loop: step controller + sync viewer at ~30 Hz."""
361- dt = 1.0 / 30.0
362- while self ._is_teleop_active and self ._controller .is_active :
363- # Check abort (Stop button / reset)
364- if self ._abort_fn is not None and self ._abort_fn ():
365- self ._controller .deactivate ()
366- break
367- t0 = time .monotonic ()
368- try :
369- from mj_manipulator .teleop import TeleopState
370-
371- with TeleopPanel ._sim_lock :
372- state = self ._controller .step ()
373- if self ._viewer is not None :
374- self ._viewer .sync ()
375- # Update status (outside lock — GUI updates are thread-safe)
376- if state == TeleopState .TRACKING :
377- self ._status_md .content = "🟢 **Tracking**"
378- elif state == TeleopState .TRACKING_COLLISION :
379- self ._status_md .content = "🔴 **Collision**"
380- elif state == TeleopState .UNREACHABLE :
381- self ._status_md .content = "🟠 **Unreachable**"
382- else :
383- self ._status_md .content = "⚪ **Idle**"
384- except Exception as e :
385- logger .warning ("Teleop step error: %s" , e )
386- break
387- elapsed = time .monotonic () - t0
388- sleep = dt - elapsed
389- if sleep > 0 :
390- time .sleep (sleep )
391-
392- # Loop exited (abort, error, or deactivate) — reset panel UI
355+ def _update_status (self , state ) -> None :
356+ """Update status indicator. Called by PhysicsEventLoop.tick()."""
357+ from mj_manipulator .teleop import TeleopState
358+
359+ if state == TeleopState .TRACKING :
360+ self ._status_md .content = "🟢 **Tracking**"
361+ elif state == TeleopState .TRACKING_COLLISION :
362+ self ._status_md .content = "🔴 **Collision**"
363+ elif state == TeleopState .UNREACHABLE :
364+ self ._status_md .content = "🟠 **Unreachable**"
365+ else :
366+ self ._status_md .content = "⚪ **Idle**"
367+
368+ def _on_teleop_error (self ) -> None :
369+ """Reset panel UI after a teleop error. Called by PhysicsEventLoop."""
393370 self ._is_teleop_active = False
394371 self ._gizmo .visible = False
395372 if self ._ghost is not None :
0 commit comments