Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions src/python/isaaccapture/cloudxr/oob_teleop_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ def client_ui_fields_from_env() -> dict:
"""Optional WebXR client UI defaults merged into hub ``config`` and bookmarks.

Keys match query params the WebXR client reads on page load
(``serverIP``, ``port``, ``codec``, ``panelHiddenAtStart``).
(``serverIP``, ``port``, ``codec``, ``panelHiddenAtStart``,
``reconnectEnabled``, ``reconnectMaxAttempts``, ``reconnectDelayMs``,
``streamAttachTimeoutMs``, ``warmupBeginTimeoutMs``, ``warmupEndTimeoutMs``).
"""
out: dict = {}
codec = os.environ.get("TELEOP_CLIENT_CODEC", "").strip()
Expand All @@ -341,6 +343,25 @@ def client_ui_fields_from_env() -> dict:
out["panelHiddenAtStart"] = True
elif ph in ("0", "false", "no", "off"):
out["panelHiddenAtStart"] = False
re_enabled = os.environ.get("TELEOP_CLIENT_RECONNECT_ENABLED", "").strip().lower()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, I would really prefer us to move away from adding environment vars to gate different behaviors. this really causes lots of unnecessary fragmentation in the user flow.

any reason we don't just make reconnect the default and only behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on slack. I think the retry is better done by the OOB system as it can do so more reliably. Though we could also do via the component as well.

if re_enabled in ("1", "true", "yes", "on"):
out["reconnectEnabled"] = True
elif re_enabled in ("0", "false", "no", "off"):
out["reconnectEnabled"] = False
for env_name, key in (
("TELEOP_CLIENT_RECONNECT_MAX_ATTEMPTS", "reconnectMaxAttempts"),
("TELEOP_CLIENT_RECONNECT_DELAY_MS", "reconnectDelayMs"),
("TELEOP_CLIENT_STREAM_ATTACH_TIMEOUT_MS", "streamAttachTimeoutMs"),
("TELEOP_CLIENT_WARMUP_BEGIN_TIMEOUT_MS", "warmupBeginTimeoutMs"),
("TELEOP_CLIENT_WARMUP_END_TIMEOUT_MS", "warmupEndTimeoutMs"),
):
raw = os.environ.get(env_name, "").strip()
if not raw:
continue
try:
out[key] = int(raw)
except ValueError:
continue
return out


Expand Down Expand Up @@ -377,10 +398,12 @@ def build_headset_bookmark_url(

Set *oob_enable* to ``False`` to omit ``oobEnable`` (and the control token,
which only authenticates hub operations). The remaining params —
``serverIP``, ``port``, ``codec``, ``panelHiddenAtStart`` — are plain form
overrides the client honours either way, so the headset still lands with
the right streaming target pre-filled. Callers must not disable OOB while
the control hub is down: without a hub, ``/oob/v1/ws`` is proxied to the
``serverIP``, ``port``, ``codec``, ``panelHiddenAtStart``, ``reconnectEnabled``,
``reconnectMaxAttempts``, ``reconnectDelayMs``, ``streamAttachTimeoutMs``,
``warmupBeginTimeoutMs``, ``warmupEndTimeoutMs`` — are plain form overrides
the client honours either way, so the headset still lands with the right
streaming target pre-filled. Callers must not disable OOB while the
control hub is down: without a hub, ``/oob/v1/ws`` is proxied to the
CloudXR streaming backend rather than refused.

A HashRouter fragment is appended at the end when ``TELEOP_CLIENT_ROUTE``
Expand All @@ -405,6 +428,21 @@ def build_headset_bookmark_url(
v = cfg.get("panelHiddenAtStart")
if isinstance(v, bool):
params["panelHiddenAtStart"] = "true" if v else "false"
v = cfg.get("reconnectEnabled")
if isinstance(v, bool):
params["reconnectEnabled"] = "true" if v else "false"
for key in (
"reconnectMaxAttempts",
"reconnectDelayMs",
"streamAttachTimeoutMs",
"warmupBeginTimeoutMs",
"warmupEndTimeoutMs",
):
v = cfg.get(key)
if isinstance(v, bool):
continue # bool is an int subclass; exclude it from the numeric fields above.
if isinstance(v, int):
params[key] = str(v)
v = cfg.get("turnServer")
if v is not None and str(v).strip() != "":
params["turnServer"] = str(v).strip()
Expand Down
30 changes: 30 additions & 0 deletions tests/python/core/cloudxr/test_oob_teleop_adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,36 @@ def test_build_teleop_url_host_client_uses_resolved_proxy_host_and_port(
assert "port=49322" in url


def test_build_teleop_url_forwards_reliability_config_from_env(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""TELEOP_CLIENT_* reconnect/warm-up env vars reach the bookmark URL end to end.

Without this, a real headset launch leaves reconnectEnabled/streamAttachTimeoutMs/
warmupBeginTimeoutMs/warmupEndTimeoutMs at their client-side defaults no matter what
the operator sets in the environment - client_ui_fields_from_env() is a generic dict
merge (build_teleop_url -> build_headset_bookmark_url), so this exercises the whole
chain rather than just the two boundary functions in test_oob_teleop_env.py.
"""
from cloudxr_py_test_ns.oob_teleop_adb import build_teleop_url

monkeypatch.delenv("TELEOP_WEB_CLIENT_BASE", raising=False)
monkeypatch.setenv("PROXY_PORT", "48322")
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_ENABLED", "true")
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_MAX_ATTEMPTS", "5")
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_DELAY_MS", "2500")
monkeypatch.setenv("TELEOP_CLIENT_STREAM_ATTACH_TIMEOUT_MS", "90000")
monkeypatch.setenv("TELEOP_CLIENT_WARMUP_BEGIN_TIMEOUT_MS", "8000")
monkeypatch.setenv("TELEOP_CLIENT_WARMUP_END_TIMEOUT_MS", "20000")
url = build_teleop_url(resolved_port=49322, usb_local=True)
assert "reconnectEnabled=true" in url
assert "reconnectMaxAttempts=5" in url
assert "reconnectDelayMs=2500" in url
assert "streamAttachTimeoutMs=90000" in url
assert "warmupBeginTimeoutMs=8000" in url
assert "warmupEndTimeoutMs=20000" in url


@patch("cloudxr_py_test_ns.oob_teleop_adb.adb_device_state", return_value="device")
@patch("cloudxr_py_test_ns.oob_teleop_adb.subprocess.run")
def test_setup_adb_reverse_ports_uses_resolved_proxy_port(
Expand Down
86 changes: 86 additions & 0 deletions tests/python/core/cloudxr/test_oob_teleop_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def clear_teleop_env(monkeypatch: pytest.MonkeyPatch) -> None:
"TELEOP_STREAM_PORT",
"TELEOP_CLIENT_CODEC",
"TELEOP_CLIENT_PANEL_HIDDEN_AT_START",
"TELEOP_CLIENT_RECONNECT_ENABLED",
"TELEOP_CLIENT_RECONNECT_MAX_ATTEMPTS",
"TELEOP_CLIENT_RECONNECT_DELAY_MS",
"TELEOP_CLIENT_STREAM_ATTACH_TIMEOUT_MS",
"TELEOP_CLIENT_WARMUP_BEGIN_TIMEOUT_MS",
"TELEOP_CLIENT_WARMUP_END_TIMEOUT_MS",
"TELEOP_CLIENT_ROUTE",
"TELEOP_WEB_CLIENT_BASE",
"TELEOP_PROXY_HOST",
Expand Down Expand Up @@ -310,6 +316,86 @@ def test_client_ui_fields_from_env_panel_hidden(
assert fields["panelHiddenAtStart"] is True


def test_client_ui_fields_from_env_reconnect_enabled(
clear_teleop_env: None, monkeypatch: pytest.MonkeyPatch
) -> None:
"""TELEOP_CLIENT_RECONNECT_ENABLED=true sets reconnectEnabled to True."""
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_ENABLED", "true")
fields = client_ui_fields_from_env()
assert fields["reconnectEnabled"] is True


def test_client_ui_fields_from_env_reconnect_disabled(
clear_teleop_env: None, monkeypatch: pytest.MonkeyPatch
) -> None:
"""TELEOP_CLIENT_RECONNECT_ENABLED=false sets reconnectEnabled to False."""
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_ENABLED", "false")
fields = client_ui_fields_from_env()
assert fields["reconnectEnabled"] is False


def test_client_ui_fields_from_env_reliability_timeouts(
clear_teleop_env: None, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The five reconnect/timeout ms/count env vars are surfaced as integer fields."""
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_MAX_ATTEMPTS", "5")
monkeypatch.setenv("TELEOP_CLIENT_RECONNECT_DELAY_MS", "2500")
monkeypatch.setenv("TELEOP_CLIENT_STREAM_ATTACH_TIMEOUT_MS", "90000")
monkeypatch.setenv("TELEOP_CLIENT_WARMUP_BEGIN_TIMEOUT_MS", "8000")
monkeypatch.setenv("TELEOP_CLIENT_WARMUP_END_TIMEOUT_MS", "20000")
fields = client_ui_fields_from_env()
assert fields["reconnectMaxAttempts"] == 5
assert fields["reconnectDelayMs"] == 2500
assert fields["streamAttachTimeoutMs"] == 90000
assert fields["warmupBeginTimeoutMs"] == 8000
assert fields["warmupEndTimeoutMs"] == 20000


def test_client_ui_fields_from_env_invalid_timeout_ignored(
clear_teleop_env: None, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A non-integer timeout env var is silently ignored rather than raising."""
monkeypatch.setenv("TELEOP_CLIENT_STREAM_ATTACH_TIMEOUT_MS", "not-a-number")
fields = client_ui_fields_from_env()
assert "streamAttachTimeoutMs" not in fields


def test_build_headset_bookmark_url_reconnect_enabled() -> None:
"""reconnectEnabled=True from stream_config is serialised as "true" in the bookmark URL."""
u = build_headset_bookmark_url(
web_client_base="https://h.test/",
stream_config={
"serverIP": "10.0.0.1",
"port": 48322,
"reconnectEnabled": True,
},
)
q = parse_qs(urlparse(u).query)
assert q["reconnectEnabled"] == ["true"]


def test_build_headset_bookmark_url_reliability_timeouts() -> None:
"""Reconnect/timeout numeric fields from stream_config are forwarded as query params."""
u = build_headset_bookmark_url(
web_client_base="https://h.test/",
stream_config={
"serverIP": "10.0.0.1",
"port": 48322,
"reconnectMaxAttempts": 5,
"reconnectDelayMs": 2500,
"streamAttachTimeoutMs": 90000,
"warmupBeginTimeoutMs": 8000,
"warmupEndTimeoutMs": 20000,
},
)
q = parse_qs(urlparse(u).query)
assert q["reconnectMaxAttempts"] == ["5"]
assert q["reconnectDelayMs"] == ["2500"]
assert q["streamAttachTimeoutMs"] == ["90000"]
assert q["warmupBeginTimeoutMs"] == ["8000"]
assert q["warmupEndTimeoutMs"] == ["20000"]


def test_default_initial_stream_config_defaults(clear_teleop_env: None) -> None:
"""default_initial_stream_config uses the supplied port and includes a serverIP."""
cfg = default_initial_stream_config(48322)
Expand Down
Loading