Skip to content

feat(oob): Adds initial oob implementation with metrics#388

Open
yanziz-nvidia wants to merge 1 commit intomainfrom
yanziz/oob-phase1
Open

feat(oob): Adds initial oob implementation with metrics#388
yanziz-nvidia wants to merge 1 commit intomainfrom
yanziz/oob-phase1

Conversation

@yanziz-nvidia
Copy link
Copy Markdown
Contributor

@yanziz-nvidia yanziz-nvidia commented Apr 11, 2026

Summary by CodeRabbit

  • New Features

    • Added out-of-band teleop control hub for managing XR headsets and operators.
    • Headsets now report streaming metrics (FPS and latency) to the control hub.
    • HTTP API endpoints to retrieve headset state and push configuration updates.
    • WebSocket-based control channel for real-time headset management.
    • Enabled via --setup-oob flag when starting the CloudXR service.
  • Documentation

    • Added comprehensive setup and usage guide for the out-of-band teleop control feature.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2436f30b-5388-482e-984c-d2b7065d10f0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces an out-of-band (OOB) teleop control infrastructure for Isaac Teleop CloudXR. A new TypeScript WebSocket client (HeadsetControlChannel) enables XR headsets to register with and communicate metrics to a Python control hub over a dedicated channel. The hub (OOBControlHub) manages headset connections, aggregates metrics by cadence, and maintains a shared stream configuration. HTTP endpoints expose hub state and allow configuration updates targeting specific headsets. Integration includes CLI flag support (--setup-oob), WSS proxy modifications to route OOB WebSocket and HTTP traffic, and WebXR client integration to connect headsets and report render/streaming performance metrics. Token-based authentication protects both WebSocket and HTTP access.

Sequence Diagram(s)

sequenceDiagram
    participant Headset as XR Headset<br/>(WebSocket Client)
    participant Hub as OOB Control Hub<br/>(WebSocket Server)
    participant Storage as Hub State<br/>(Metrics & Config)

    Headset->>Hub: WebSocket Connect
    activate Hub
    Headset->>Hub: register message<br/>(role: "headset", token?, deviceLabel?)
    Hub->>Storage: Store headset connection
    Hub->>Headset: hello message<br/>(clientId, configVersion, config)
    deactivate Hub
    
    loop Every metrics interval
        Headset->>Headset: Capture render/streaming FPS
        Headset->>Hub: clientMetrics message<br/>(cadence, timestamp, metrics)
        activate Hub
        Hub->>Storage: Update metrics by cadence
        deactivate Hub
    end
    
    Headset->>Hub: (periodic heartbeat via metrics)
    Hub->>Headset: config message<br/>(when config updated)
    Headset->>Headset: Apply new stream config
Loading
sequenceDiagram
    participant Operator as HTTP API Client<br/>(Dashboard/Script)
    participant Hub as OOB Control Hub<br/>(HTTP Handler)
    participant Storage as Hub State<br/>(Config & Headsets)
    participant Headset as Connected Headset<br/>(WebSocket)

    Operator->>Hub: GET /api/oob/v1/state<br/>(token auth)
    activate Hub
    Hub->>Storage: Read snapshot
    Hub-->>Operator: 200 OK<br/>(headsets, metrics, config, configVersion)
    deactivate Hub

    Operator->>Hub: GET /api/oob/v1/config?serverIP=x&port=y<br/>(token auth)
    activate Hub
    Hub->>Storage: Validate & merge config
    alt Config changed
        Hub->>Storage: Increment configVersion
        Hub->>Headset: config message<br/>(updated config + version)
        activate Headset
        Headset->>Headset: Apply new config
        deactivate Headset
        Hub-->>Operator: 200 OK<br/>(changed: true, configVersion)
    else No changes
        Hub-->>Operator: 200 OK<br/>(changed: false)
    end
    deactivate Hub
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: an initial out-of-band (OOB) implementation with metrics support, which is the core of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yanziz/oob-phase1

Comment @coderabbitai help to get the list of available commands and usage tips.

@yanziz-nvidia yanziz-nvidia marked this pull request as ready for review April 11, 2026 20:02
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/cloudxr/python/wss.py (1)

140-145: ⚠️ Potential issue | 🔴 Critical

/api/oob/v1/config is mutable with no auth by default.

When CONTROL_TOKEN is unset, hub.check_token() accepts every caller, and this handler applies live config changes from a simple GET query string while advertising Access-Control-Allow-Origin: *. That leaves headset reconfiguration exposed to any caller that can reach the proxy, including browser-driven cross-origin requests once the cert is trusted. Require a control token when setup_oob is enabled, and avoid state changes on GET.

Also applies to: 303-323, 500-506

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/wss.py` around lines 140 - 145, The
/api/oob/v1/config handler in src/core/cloudxr/python/wss.py currently allows
unauthenticated mutating config via GET and advertises
Access-Control-Allow-Origin: *; update the handler (the function handling the
route for "/api/oob/v1/config") so that when setup_oob is enabled it enforces
hub.check_token() and rejects requests lacking a valid CONTROL_TOKEN, and stop
performing state changes on GET requests (accept mutating operations only via
POST/PUT/ PATCH and keep GET strictly read-only). Also remove or tighten
wildcard CORS (the CORS_HEADERS entry) for mutating endpoints so that
Access-Control-Allow-Origin is not "*" for endpoints that change state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deps/cloudxr/webxr_client/helpers/controlChannel.ts`:
- Around line 157-173: The onclose handler currently always calls
_afterSocketClosed() which unconditionally schedules a reconnect; change the
onclose callback to accept the CloseEvent (e.g., (ev: CloseEvent) => { ... })
and pass ev.code into _afterSocketClosed, then update _afterSocketClosed to
accept an optional closeCode parameter and treat terminal codes (at least 1008
for policy/auth failure) as final by not scheduling reconnectTimer when
closeCode === 1008 (and still clear timers and call opts.onConnectionChange).
Keep disposed behavior and RECONNECT_DELAY_MS logic unchanged for non-terminal
codes.

In `@deps/cloudxr/webxr_client/src/App.tsx`:
- Around line 646-648: The code is reading the out-of-band auth token from the
URL (p.get('controlToken')) which leaks it; stop extracting the token from
window.location.search and instead accept the token via a non-URL channel (for
example an initialization config object, a secure parent.postMessage handshake,
or server-rendered JS variable) and pass that token into the
HeadsetControlChannel constructor. Update HeadsetControlChannel to use the
provided token for the WebSocket handshake/register payload (not from the URL)
and remove any usage of p.get('controlToken') so the token is never placed into
the URL or browser history/referrer.
- Around line 652-672: The getMetricsSnapshot callback is returning stale values
by reading renderMetrics.value and streamingMetrics.value even after the XR
session ends; update the XR lifecycle to clear/reset these refs on session end
(e.g., in the XR stop/disconnect handler or cloudXR2DUI teardown) or change
getMetricsSnapshot to check an active-session flag before returning metrics and
return an empty array when no session is active. Specifically, locate
getMetricsSnapshot, the renderMetrics and streamingMetrics refs, and the XR
session start/stop or cloudXR2DUI disconnect handlers (session end logic) and
either null/undefined those refs on end or gate getMetricsSnapshot behind the
session active boolean so no stale FPS/latency samples are emitted after session
exit.

In `@docs/source/references/oob_teleop_control.rst`:
- Around line 29-32: The docs currently state the WebXR client reports streaming
metrics "every frame"; update the description to say the client sends periodic
snapshots at the configured metricsIntervalMs (default 500 ms) as clientMetrics
messages instead of per-render-frame sends; adjust the XR headset / Isaac Teleop
WebXR client text and any other occurrences (e.g., the block referencing
metricsIntervalMs and clientMetrics) to mention periodic reporting at
metricsIntervalMs rather than per-frame reporting.
- Around line 36-39: Update the OOB teleop control docs to remove the incorrect
WebSocket config path: edit the bullet that currently reads "Reads state via
HTTP, optionally pushes config via HTTP or WebSocket" (and the similar text at
the other occurrence) so it only mentions HTTP (config via GET
/api/oob/v1/config) and remove any reference to a WebSocket `setConfig` path;
keep references to headset `register` and `clientMetrics` for the WebSocket
protocol as-is.
- Around line 100-101: The docs claim the /state response contains "dashboards",
but OOBControlHub.get_snapshot() actually returns only updatedAt, configVersion,
config, and headsets; update the documentation text for the /state endpoint to
remove "dashboards" and list the actual returned fields (updatedAt,
configVersion, config, headsets) so the docs match the
OOBControlHub.get_snapshot() response structure.

In `@src/core/cloudxr/python/oob_teleop_hub.py`:
- Around line 219-243: In _merge_stream_config: resolve targets before mutating
shared state — first collect all_headsets and, if target_id is provided, find
targets and return ("missing", target_id) if none; only after target resolution
compute merged values but do NOT assign into self._stream_config for targeted
updates; for target-specific pushes create per-target snapshots by applying
new_config onto a copy of the current self._stream_config (e.g., snapshot =
dict(self._stream_config); overridden = {**snapshot, **new_config}) and compare
overridden vs snapshot to decide noop vs push for those targets; only update
self._stream_config and increment self._config_version when target_id is None
(global update) and then return ("push", version, snapshot, targets) as before.
Ensure all uses reference _merge_stream_config, self._stream_config,
self._config_version, target_id, and targets.

---

Outside diff comments:
In `@src/core/cloudxr/python/wss.py`:
- Around line 140-145: The /api/oob/v1/config handler in
src/core/cloudxr/python/wss.py currently allows unauthenticated mutating config
via GET and advertises Access-Control-Allow-Origin: *; update the handler (the
function handling the route for "/api/oob/v1/config") so that when setup_oob is
enabled it enforces hub.check_token() and rejects requests lacking a valid
CONTROL_TOKEN, and stop performing state changes on GET requests (accept
mutating operations only via POST/PUT/ PATCH and keep GET strictly read-only).
Also remove or tighten wildcard CORS (the CORS_HEADERS entry) for mutating
endpoints so that Access-Control-Allow-Origin is not "*" for endpoints that
change state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 116b174b-1252-43ab-b2fa-8eb9e32b1de6

📥 Commits

Reviewing files that changed from the base of the PR and between af72817 and 99d1fda.

📒 Files selected for processing (12)
  • deps/cloudxr/webxr_client/helpers/controlChannel.ts
  • deps/cloudxr/webxr_client/src/App.tsx
  • docs/source/index.rst
  • docs/source/references/oob_teleop_control.rst
  • src/core/cloudxr/python/CMakeLists.txt
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/launcher.py
  • src/core/cloudxr/python/oob_teleop_hub.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/conftest.py
  • src/core/cloudxr_tests/python/pyproject.toml
  • src/core/cloudxr_tests/python/test_oob_teleop_hub.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant