Skip to content

Commit b43ef81

Browse files
authored
Per-arm teleop preemption via ownership registry (#19)
_activate_teleop() now uses registry.preempt() to abort only the executing arm's trajectory, leaving other arms unaffected. Falls back to global request_abort() when no registry is available. _deactivate_teleop() releases ownership via registry.release(). Part of bimanual concurrent control (mj_manipulator #54, #56, #57).
1 parent 508ff90 commit b43ef81

1 file changed

Lines changed: 22 additions & 8 deletions

File tree

src/mj_viser/teleop_panel.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)