@@ -203,6 +203,7 @@ def __init__(
203203 clear_abort_fn : object | None = None ,
204204 request_abort_fn : object | None = None ,
205205 event_loop : object | None = None ,
206+ ownership : object | None = None ,
206207 ):
207208 self ._arm = arm
208209 self ._controller = controller
@@ -214,6 +215,7 @@ def __init__(
214215 self ._clear_abort_fn = clear_abort_fn # callable → None, clears abort
215216 self ._request_abort_fn = request_abort_fn # callable → None, triggers abort
216217 self ._event_loop = event_loop # PhysicsEventLoop — steps teleop from main thread
218+ self ._ownership = ownership # OwnershipRegistry for per-arm preemption
217219 self ._gizmo = None
218220 self ._ghost = None
219221 self ._is_teleop_active = False
@@ -318,14 +320,22 @@ def _on_record(event) -> None:
318320 # _teleop_loop thread to avoid recursion (step → sync → on_sync).
319321
320322 def _activate_teleop (self ) -> None :
321- # Abort any running trajectory so execute() yields and the event
322- # loop can process our activation on the main thread.
323- if self ._request_abort_fn is not None :
323+ arm_name = self ._arm .config .name
324+
325+ if self ._ownership is not None :
326+ # Per-arm preemption: only aborts THIS arm's trajectory,
327+ # other arms continue unaffected.
328+ from mj_manipulator .ownership import OwnerKind
329+
330+ self ._ownership .preempt (arm_name , OwnerKind .TELEOP , self ._controller )
331+ elif self ._request_abort_fn is not None :
332+ # Legacy fallback: global abort (aborts all arms)
324333 self ._request_abort_fn ()
325334
326335 def _do_activate ():
327- # Clear the abort we just triggered (or any stale one)
328- if self ._clear_abort_fn is not None :
336+ if self ._ownership is not None :
337+ self ._ownership .clear_abort (arm_name )
338+ elif self ._clear_abort_fn is not None :
329339 self ._clear_abort_fn ()
330340 ee_pose = self ._controller .activate ()
331341 self ._is_teleop_active = True
@@ -342,9 +352,9 @@ def _do_activate():
342352 self ._event_loop .register_teleop (self ._controller , self )
343353
344354 if self ._event_loop is not None :
345- # Submit to run on the physics thread after execute() yields.
346- # This blocks the viser callback thread until activation
347- # completes — that's fine, it's a button click handler .
355+ # Submit to run on the physics thread. In tick-driven mode,
356+ # commands are processed immediately (non-blocking), so this
357+ # completes on the next tick .
348358 self ._event_loop .submit (_do_activate ).result ()
349359 else :
350360 _do_activate ()
@@ -355,6 +365,10 @@ def _deactivate_teleop(self) -> None:
355365 self ._event_loop .unregister_teleop (self ._controller )
356366 self ._controller .deactivate ()
357367
368+ if self ._ownership is not None :
369+ arm_name = self ._arm .config .name
370+ self ._ownership .release (arm_name , self ._controller )
371+
358372 self ._gizmo .visible = False
359373 if self ._ghost is not None :
360374 self ._ghost .set_visible (False )
0 commit comments