Skip to content

Commit 657a9b1

Browse files
committed
Use background thread for teleop step loop
The gizmo callback runs in Viser's thread and can't safely touch MuJoCo data. Now the callback only buffers the target pose, and a background thread at ~30Hz calls controller.step() + viewer.sync(). The thread starts on activate and stops on deactivate.
1 parent a4c2bfa commit 657a9b1

1 file changed

Lines changed: 27 additions & 7 deletions

File tree

src/mj_viser/teleop_panel.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from __future__ import annotations
2525

2626
import logging
27+
import threading
28+
import time
2729
from typing import TYPE_CHECKING
2830

2931
import mujoco
@@ -251,12 +253,8 @@ def _on_gizmo_update(event) -> None:
251253
[2*(x*z - w*y), 2*(y*z + w*x), 1 - 2*(x*x + y*y)],
252254
])
253255
pose[:3, 3] = event.target.position
256+
# Only buffer the target — stepping happens in _teleop_loop thread
254257
self._controller.set_target_pose(pose)
255-
# Step immediately — no continuous sync loop in IPython mode
256-
state = self._controller.step()
257-
# Sync viewer so the arm movement is visible
258-
if self._viewer is not None:
259-
self._viewer.sync()
260258

261259
# GUI controls
262260
self._activate_btn = gui.add_button(
@@ -334,15 +332,20 @@ def _activate_teleop(self) -> None:
334332
self._gizmo.visible = True
335333

336334
if self._ghost is not None:
337-
# Ghost is a child of gizmo — just make it visible
338335
self._ghost.set_visible(True)
339336

340337
self._activate_btn.name = f"Deactivate Teleop ({self._arm_label})"
341338
self._activate_btn.color = "red"
342339

340+
# Start background loop that steps the controller + syncs viewer
341+
self._teleop_thread = threading.Thread(
342+
target=self._teleop_loop, daemon=True,
343+
)
344+
self._teleop_thread.start()
345+
343346
def _deactivate_teleop(self) -> None:
347+
self._is_teleop_active = False # signals thread to stop
344348
self._controller.deactivate()
345-
self._is_teleop_active = False
346349

347350
if self._gizmo is not None:
348351
self._gizmo.visible = False
@@ -351,3 +354,20 @@ def _deactivate_teleop(self) -> None:
351354

352355
self._activate_btn.name = f"Activate Teleop ({self._arm_label})"
353356
self._activate_btn.color = "green"
357+
358+
def _teleop_loop(self) -> None:
359+
"""Background loop: step controller + sync viewer at ~30 Hz."""
360+
dt = 1.0 / 30.0
361+
while self._is_teleop_active:
362+
t0 = time.monotonic()
363+
try:
364+
self._controller.step()
365+
if self._viewer is not None:
366+
self._viewer.sync()
367+
except Exception as e:
368+
logger.warning("Teleop step error: %s", e)
369+
break
370+
elapsed = time.monotonic() - t0
371+
sleep = dt - elapsed
372+
if sleep > 0:
373+
time.sleep(sleep)

0 commit comments

Comments
 (0)