Skip to content

Commit 8bd1884

Browse files
committed
Pass the visualizer windows to the script input helpers explicitly
The teleop session now reads keyboard input only from the visualizers it is given. open_teleop_device takes them and opens the device through select_teleop_cfg and create_teleop_device, and add_key_callbacks takes them instead of looking up the simulation. The replay, annotate, consolidated Mimic and pick-and-place scripts pass sim.visualizers.
1 parent 4bb6324 commit 8bd1884

8 files changed

Lines changed: 55 additions & 43 deletions

File tree

‎examples/demos/pick_and_place.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,10 @@ def main() -> None:
333333
# the keyboard runs through Isaac Capture, which the teleop extra installs
334334
from isaaclab_teleop.script_input import open_teleop_device
335335

336-
teleop = teleop_device.enter_context(open_teleop_device(teleop_cfg))
336+
teleop = teleop_device.enter_context(open_teleop_device(teleop_cfg, pick_and_place.sim.visualizers))
337337
add_key_callbacks(
338-
{"N": lambda: pick_and_place.auto_aim(cube=True), "M": lambda: pick_and_place.auto_aim(cube=False)}
338+
pick_and_place.sim.visualizers,
339+
{"N": lambda: pick_and_place.auto_aim(cube=True), "M": lambda: pick_and_place.auto_aim(cube=False)},
339340
)
340341
print(teleop)
341342
print("Pick up the purple cube and drop it on the red sphere, in ALL environments at once.")

‎scripts/imitation_learning/isaaclab_mimic/annotate_demos.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def annotate_dataset(env_cfg, dataset_file_handler: HDF5DatasetFileHandler, succ
288288
key_callbacks = {"N": play_cb, "B": pause_cb, "Q": skip_episode_cb}
289289
if not args_cli.auto:
290290
key_callbacks["S"] = mark_subtask_cb
291-
add_key_callbacks(key_callbacks)
291+
add_key_callbacks(env.unwrapped.sim.visualizers, key_callbacks)
292292

293293
# simulate environment -- run everything in inference mode
294294
exported_episode_count = 0

‎scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,9 @@ def run_consolidated_demo(env_cfg, success_term):
458458
if args_cli.teleop_env_index is not None:
459459
from isaaclab_teleop.script_input import open_teleop_device
460460

461-
teleop_interface = teleop_device.enter_context(open_teleop_device(teleop_device_cfg()))
461+
teleop_interface = teleop_device.enter_context(
462+
open_teleop_device(teleop_device_cfg(), env.unwrapped.sim.visualizers)
463+
)
462464

463465
# make data generator object
464466
data_generator_asyncio_tasks = []

‎scripts/tools/replay_demos.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def replay_dataset(
311311
# create environment from loaded config
312312
env = gym.make(args_cli.task, cfg=env_cfg).unwrapped
313313

314-
add_key_callbacks({"N": play_cb, "B": pause_cb})
314+
add_key_callbacks(env.unwrapped.sim.visualizers, {"N": play_cb, "B": pause_cb})
315315
print('Press "B" to pause and "N" to resume the replayed actions.')
316316

317317
# Determine if state validation should be conducted

‎source/isaaclab/isaaclab/visualizers/key_event_source.py‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from __future__ import annotations
99

1010
import logging
11-
from collections.abc import Callable, Mapping
11+
from collections.abc import Callable, Mapping, Sequence
12+
from typing import TYPE_CHECKING
13+
14+
if TYPE_CHECKING:
15+
from .base_visualizer import BaseVisualizer
1216

1317
logger = logging.getLogger(__name__)
1418

@@ -121,24 +125,24 @@ def _emit_focus_lost(self) -> None:
121125
_call(subscription._on_focus_lost)
122126

123127

124-
def add_key_callbacks(callbacks: Mapping[str, Callable[[], None]]) -> Callable[[], None]:
128+
def add_key_callbacks(
129+
visualizers: Sequence[BaseVisualizer], callbacks: Mapping[str, Callable[[], None]]
130+
) -> Callable[[], None]:
125131
"""Call ``callbacks[key]`` when ``key`` is pressed in a visualizer window, e.g. for a script's hotkeys.
126132
127133
Keys are letters or digits (``"N"``, ``"1"``) or W3C ``KeyboardEvent.code`` strings (``"Escape"``).
128134
Key repeat is not a press, and a press reported by several visualizers (Kit visualizers share the
129-
app window) fires once. Reads every visualizer window open when called; the callbacks end when
130-
the visualizers close, or when the returned function is called.
135+
app window) fires once. The callbacks end when the visualizers close, or when the returned
136+
function is called.
131137
132138
Args:
139+
visualizers: The windows to read, usually ``sim.visualizers``.
133140
callbacks: The function to call for each key. Takes no arguments.
134141
135142
Returns:
136143
A function that removes the callbacks.
137144
"""
138-
from isaaclab.sim import SimulationContext
139-
140145
by_code = {_w3c_code(key): func for key, func in callbacks.items()}
141-
sim = SimulationContext.instance()
142146
# shared by every window, so a press several visualizers report fires once
143147
held: set[str] = set()
144148

@@ -151,7 +155,7 @@ def on_key(code: str, pressed: bool) -> None:
151155
by_code[code]()
152156

153157
subscriptions = []
154-
for visualizer in sim.visualizers if sim is not None else ():
158+
for visualizer in visualizers:
155159
source = visualizer.key_event_source
156160
if source is not None:
157161
subscriptions.append(source.add_key_listener(on_key, held.clear))

‎source/isaaclab/test/visualizers/test_key_event_source.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import logging
1414
from types import SimpleNamespace
15-
from unittest.mock import patch
1615

1716
import pytest
1817

@@ -149,13 +148,15 @@ def source():
149148
sim = SimpleNamespace(
150149
visualizers=[SimpleNamespace(key_event_source=key_event_source), SimpleNamespace(key_event_source=None)]
151150
)
152-
with patch("isaaclab.sim.SimulationContext.instance", return_value=sim):
153-
yield key_event_source
151+
return key_event_source, sim.visualizers
154152

155153

156154
def test_key_callbacks_fire_once_per_press(source):
157155
pressed = []
158-
add_key_callbacks({"N": lambda: pressed.append("N"), "1": lambda: pressed.append("1"), "Escape": lambda: 0})
156+
source, visualizers = source
157+
add_key_callbacks(
158+
visualizers, {"N": lambda: pressed.append("N"), "1": lambda: pressed.append("1"), "Escape": lambda: 0}
159+
)
159160

160161
source._emit_key("KeyN", True)
161162
source._emit_key("KeyN", True) # key repeat
@@ -168,15 +169,17 @@ def test_key_callbacks_fire_once_per_press(source):
168169

169170
def test_focus_loss_ends_a_held_key(source):
170171
pressed = []
171-
add_key_callbacks({"N": lambda: pressed.append("N")})
172+
source, visualizers = source
173+
add_key_callbacks(visualizers, {"N": lambda: pressed.append("N")})
172174
source._emit_key("KeyN", True)
173175
source._emit_focus_lost()
174176
source._emit_key("KeyN", True)
175177
assert pressed == ["N", "N"]
176178

177179

178180
def test_removing_the_key_callbacks_unsubscribes(source):
179-
remove = add_key_callbacks({"N": lambda: None})
181+
source, visualizers = source
182+
remove = add_key_callbacks(visualizers, {"N": lambda: None})
180183
assert len(source._subscriptions) == 1
181184
remove()
182185
assert source._subscriptions == []

‎source/isaaclab_teleop/isaaclab_teleop/script_input.py‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@
88
from __future__ import annotations
99

1010
import contextlib
11-
from collections.abc import Iterator
11+
from collections.abc import Iterator, Sequence
1212
from typing import TYPE_CHECKING, Any
1313

14-
from .device_selection import _require_isaaccapture, pipeline_from_device_cfg
14+
from .device_selection import create_teleop_device, select_teleop_cfg
15+
from .isaac_teleop_cfg import CLOUDXR_STANDALONE_ENV, IsaacTeleopCfg
1516

1617
if TYPE_CHECKING:
1718
from isaaclab.devices import DeviceCfg
19+
from isaaclab.visualizers import BaseVisualizer
1820

1921

2022
@contextlib.contextmanager
21-
def open_teleop_device(device_cfg: DeviceCfg) -> Iterator[Any]:
23+
def open_teleop_device(device_cfg: DeviceCfg, visualizers: Sequence[BaseVisualizer]) -> Iterator[Any]:
2224
"""Open the keyboard, gamepad or SpaceMouse a device config describes, through Isaac Capture.
2325
2426
The device runs as an Isaac Capture pipeline, started without a headset on the standalone CloudXR
@@ -28,6 +30,7 @@ def open_teleop_device(device_cfg: DeviceCfg) -> Iterator[Any]:
2830
2931
Args:
3032
device_cfg: A keyboard, gamepad or SpaceMouse config, e.g. :class:`~isaaclab.devices.Se3KeyboardCfg`.
33+
visualizers: The windows that supply keyboard input, usually ``sim.visualizers``.
3134
3235
Yields:
3336
The open device.
@@ -36,15 +39,11 @@ def open_teleop_device(device_cfg: DeviceCfg) -> Iterator[Any]:
3639
ImportError: If Isaac Capture is not installed.
3740
ValueError: If no Isaac Capture pipeline serves the device config.
3841
"""
39-
_require_isaaccapture()
40-
pipeline = pipeline_from_device_cfg(device_cfg, device_cfg.sim_device)
41-
if pipeline is None:
42+
cfg = select_teleop_cfg(None, "device", {"device": device_cfg}, device_cfg.sim_device)
43+
if not isinstance(cfg, IsaacTeleopCfg):
4244
raise ValueError(f"No Isaac Capture pipeline serves {type(device_cfg).__name__}")
43-
from .isaac_teleop_cfg import CLOUDXR_STANDALONE_ENV
44-
from .isaac_teleop_device import create_isaac_teleop_device
45-
46-
with create_isaac_teleop_device(
47-
pipeline, use_kit_xr_bridge=False, cloudxr_env_file=CLOUDXR_STANDALONE_ENV
45+
with create_teleop_device(
46+
cfg, use_kit_xr_bridge=False, cloudxr_env_file=CLOUDXR_STANDALONE_ENV, visualizers=visualizers
4847
) as device:
4948
device.request_start()
5049
yield device

‎source/isaaclab_teleop/test/test_script_input.py‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,39 @@
99

1010
from __future__ import annotations
1111

12-
from types import SimpleNamespace
13-
from unittest.mock import MagicMock, patch
12+
from unittest.mock import MagicMock
1413

1514
import isaaclab_teleop.device_selection as device_selection
1615
import isaaclab_teleop.script_input as script_input
1716
import pytest
18-
from isaaclab_teleop.isaac_teleop_cfg import CLOUDXR_STANDALONE_ENV
17+
from isaaclab_teleop.isaac_teleop_cfg import CLOUDXR_STANDALONE_ENV, IsaacTeleopCfg
1918
from isaaclab_teleop.script_input import open_teleop_device
2019

20+
from isaaclab.devices import Se3KeyboardCfg
21+
2122
pytestmark = pytest.mark.unit
2223

2324

2425
def test_without_isaacteleop_opening_a_device_raises(monkeypatch):
25-
monkeypatch.setattr(device_selection, "_isaacteleop_available", lambda: False)
26+
monkeypatch.setattr(device_selection.importlib.util, "find_spec", lambda _: None)
2627
with pytest.raises(ImportError, match="teleop extra"):
27-
with open_teleop_device(SimpleNamespace(name="keyboard", sim_device="cpu")):
28+
with open_teleop_device(Se3KeyboardCfg(), visualizers=()):
2829
pass
2930

3031

3132
def test_with_isaacteleop_the_pipeline_runs_headset_free(monkeypatch):
32-
monkeypatch.setattr(device_selection, "_isaacteleop_available", lambda: True)
33-
monkeypatch.setattr(script_input, "pipeline_from_device_cfg", lambda cfg, sim_device: f"pipeline:{cfg.name}")
33+
cfg = IsaacTeleopCfg()
34+
monkeypatch.setattr(script_input, "select_teleop_cfg", lambda env_cfg, name, defaults, sim_device: cfg)
3435
device = MagicMock()
3536
device.__enter__.return_value = device
36-
with patch("isaaclab_teleop.isaac_teleop_device.create_isaac_teleop_device", return_value=device) as create:
37-
with open_teleop_device(SimpleNamespace(name="keyboard", sim_device="cpu")) as opened:
38-
assert opened is device
39-
device.request_start.assert_called_once()
40-
device.__exit__.assert_not_called()
37+
create = MagicMock(return_value=device)
38+
monkeypatch.setattr(script_input, "create_teleop_device", create)
39+
visualizers = [object()]
40+
with open_teleop_device(Se3KeyboardCfg(), visualizers) as opened:
41+
assert opened is device
42+
device.request_start.assert_called_once()
43+
device.__exit__.assert_not_called()
4144
create.assert_called_once_with(
42-
"pipeline:keyboard", use_kit_xr_bridge=False, cloudxr_env_file=CLOUDXR_STANDALONE_ENV
45+
cfg, use_kit_xr_bridge=False, cloudxr_env_file=CLOUDXR_STANDALONE_ENV, visualizers=visualizers
4346
)
4447
device.__exit__.assert_called_once()

0 commit comments

Comments
 (0)