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
68 changes: 68 additions & 0 deletions deps/cloudxr/openxr_extensions/XR_NV_opaque_data_channel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0 OR MIT
/*!
* @file
* @brief Header for XR_NV_opaque_data_channel extension.
*/
#ifndef XR_NV_OPAQUE_DATA_CHANNEL_H
#define XR_NV_OPAQUE_DATA_CHANNEL_H 1

#include "openxr_extension_helpers.h"

#ifdef __cplusplus
extern "C" {
#endif

#define XR_NV_opaque_data_channel 1
#define XR_NV_opaque_data_channel_SPEC_VERSION 1
#define XR_NV_OPAQUE_DATA_CHANNEL_EXTENSION_NAME "XR_NV_opaque_data_channel"

XR_DEFINE_HANDLE(XrOpaqueDataChannelNV)

XR_STRUCT_ENUM(XR_TYPE_OPAQUE_DATA_CHANNEL_CREATE_INFO_NV, 1000526001);
XR_STRUCT_ENUM(XR_TYPE_OPAQUE_DATA_CHANNEL_STATE_NV, 1000526002);

XR_RESULT_ENUM(XR_ERROR_CHANNEL_ALREADY_CREATED_NV, -1000526000);
XR_RESULT_ENUM(XR_ERROR_CHANNEL_NOT_CONNECTED_NV, -1000526001);

typedef enum XrOpaqueDataChannelStatusNV {
XR_OPAQUE_DATA_CHANNEL_STATUS_CONNECTING_NV = 0,
XR_OPAQUE_DATA_CHANNEL_STATUS_CONNECTED_NV = 1,
XR_OPAQUE_DATA_CHANNEL_STATUS_SHUTTING_NV = 2,
XR_OPAQUE_DATA_CHANNEL_STATUS_DISCONNECTED_NV = 3,
XR_OPAQUE_DATA_CHANNEL_STATUS_MAX_ENUM = 0x7FFFFFFF,
} XrOpaqueDataChannelStatusNV;

typedef struct XrOpaqueDataChannelCreateInfoNV {
XrStructureType type;
const void* next;
XrSystemId systemId;
XrUuidEXT uuid;
} XrOpaqueDataChannelCreateInfoNV;

typedef struct XrOpaqueDataChannelStateNV {
XrStructureType type;
void* next;
XrOpaqueDataChannelStatusNV state;
} XrOpaqueDataChannelStateNV;

typedef XrResult(XRAPI_PTR* PFN_xrCreateOpaqueDataChannelNV)(XrInstance instance,
const XrOpaqueDataChannelCreateInfoNV* createInfo,
XrOpaqueDataChannelNV* opaqueDataChannel);
typedef XrResult(XRAPI_PTR* PFN_xrDestroyOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel);
typedef XrResult(XRAPI_PTR* PFN_xrGetOpaqueDataChannelStateNV)(XrOpaqueDataChannelNV opaqueDataChannel,
XrOpaqueDataChannelStateNV* state);
typedef XrResult(XRAPI_PTR* PFN_xrSendOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel,
uint32_t opaqueDataInputCount,
const uint8_t* opaqueDatas);
typedef XrResult(XRAPI_PTR* PFN_xrReceiveOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel,
uint32_t opaqueDataCapacityInput,
uint32_t* opaqueDataCountOutput,
uint8_t* opaqueDatas);
typedef XrResult(XRAPI_PTR* PFN_xrShutdownOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel);

#ifdef __cplusplus
}
#endif

#endif
126 changes: 126 additions & 0 deletions examples/teleop_session_manager/python/message_channel_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Set executable bit for shebang script.

Line [1] uses a shebang, but CI already reports this file is not executable. Please commit mode change to unblock pre-commit.

✅ Fix command
chmod +x examples/teleop_session_manager/python/message_channel_example.py
git add --chmod=+x examples/teleop_session_manager/python/message_channel_example.py
🧰 Tools
🪛 GitHub Actions: Run linters using pre-commit

[error] 1-1: pre-commit hook 'check-shebang-scripts-are-executable' failed (exit code 1): file has a shebang but is not marked executable. Suggested fix: 'chmod +x examples/teleop_session_manager/python/message_channel_example.py' (or adjust executable bit in git).

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

In `@examples/teleop_session_manager/python/message_channel_example.py` at line 1,
The file starting with the shebang "#!/usr/bin/env python3" needs its executable
permission set so CI/pre-commit stops failing; run chmod +x on the script and
stage the permission change (e.g., git add --chmod=+x) so the repository records
the new executable bit for this file.

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""
Message channel example using TeleopSession + retargeting source/sink nodes.

Behavior:
- Prints any incoming messages each frame.
- Once channel status is CONNECTED, sends one message every second.
"""

import argparse
import sys
import time
import uuid

from isaacteleop.retargeting_engine.deviceio_source_nodes import (
MessageChannelConnectionStatus,
message_channel_config,
)
from isaacteleop.retargeting_engine.interface import TensorGroup
from isaacteleop.schema import MessageChannelMessages, MessageChannelMessagesTrackedT
from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig


def _parse_uuid_bytes(uuid_text: str) -> bytes:
"""Parse canonical UUID text to 16-byte payload."""
return uuid.UUID(uuid_text).bytes
Comment on lines +27 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Python argparse, is using a custom type=callable that raisesargparse.ArgumentTypeErrorthe recommended way to validate a UUID argument instead of callinguuid.UUID(...)afterparse_args()and lettingValueError escape?

💡 Result:

Yes—for a “this argument must be a UUID” check, using type= (a callable that either returns a uuid.UUID or raises argparse.ArgumentTypeError) is the argparse-idiomatic approach, because argparse’s type hook is explicitly meant to perform “type-checking and type conversions” during parsing, and it will catch ArgumentTypeError/TypeError/ValueError and turn them into a proper CLI error message. It also notes type should be used for simple conversions, with more complex validation done downstream—UUID parsing fits the “simple conversion” case. [1]

uuid.UUID(...) raises ValueError for malformed UUID strings (e.g., “badly formed hexadecimal UUID string”). [2] You can let that ValueError occur inside your type= callable (argparse will catch it), but raising ArgumentTypeError is the way to control the exact user-facing error text. [1]

Example:

import argparse, uuid

def uuid_type(s: str) -> uuid.UUID:
    try:
        return uuid.UUID(s)
    except ValueError as e:
        raise argparse.ArgumentTypeError(f"invalid UUID: {s!r}") from e

parser = argparse.ArgumentParser()
parser.add_argument("--id", type=uuid_type, required=True)
args = parser.parse_args()

What’s not recommended (for user-friendly CLIs) is waiting until after parse_args() and letting a raw ValueError escape, because that bypasses argparse’s normal “nice” error reporting path. [1]

References:
[1] Python argparse docs (type= conversion/validation; caught exceptions; guidance on simple vs downstream validation): https://docs.python.org/3/library/argparse.html#type
[2] CPython uuid.UUID implementation raising ValueError for malformed hex input: https://github.com/python/cpython/blob/v3.14.3/Lib/uuid.py


🏁 Script executed:

fd message_channel_example.py

Repository: NVIDIA/IsaacTeleop

Length of output: 127


🏁 Script executed:

cat -n examples/teleop_session_manager/python/message_channel_example.py

Repository: NVIDIA/IsaacTeleop

Length of output: 4750


Validate --channel-uuid during argparse parsing.

Invalid UUID input currently fails after parse_args() (line 63), producing a raw ValueError traceback instead of a clear CLI error tied to the flag. Move the conversion into type= (line 45) and raise argparse.ArgumentTypeError to provide proper argparse error handling.

Suggested update
 def _parse_uuid_bytes(uuid_text: str) -> bytes:
     """Parse canonical UUID text to 16-byte payload."""
-    return uuid.UUID(uuid_text).bytes
+    try:
+        return uuid.UUID(uuid_text).bytes
+    except ValueError as exc:
+        raise argparse.ArgumentTypeError(
+            "--channel-uuid must be a valid UUID"
+        ) from exc
@@
     parser.add_argument(
         "--channel-uuid",
-        type=str,
+        type=_parse_uuid_bytes,
         required=True,
         help="Message channel UUID (canonical form, e.g. 550e8400-e29b-41d4-a716-446655440000)",
     )
@@
-    channel_uuid = _parse_uuid_bytes(args.channel_uuid)
+    channel_uuid = args.channel_uuid
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/teleop_session_manager/python/message_channel_example.py` around
lines 27 - 29, Move UUID validation into argparse by using _parse_uuid_bytes as
the type= callable for the --channel-uuid argument so invalid input yields a
proper argparse error instead of a raw ValueError; update _parse_uuid_bytes to
catch ValueError from uuid.UUID(uuid_text) and re-raise
argparse.ArgumentTypeError with a clear message (referencing the flag name),
then pass that function to add_argument for --channel-uuid so parse_args() will
surface friendly errors.



def _enqueue_outbound_message(sink, payload: bytes) -> None:
"""Push one outbound message through MessageChannelSink."""
tg = TensorGroup(sink.input_spec()["messages_tracked"])
tg[0] = MessageChannelMessagesTrackedT([MessageChannelMessages(payload)])
sink.compute({"messages_tracked": tg}, {})


def main() -> int:
parser = argparse.ArgumentParser(
description="Message channel TeleopSession example"
)
parser.add_argument(
"--channel-uuid",
type=str,
required=True,
help="Message channel UUID (canonical form, e.g. 550e8400-e29b-41d4-a716-446655440000)",
)
parser.add_argument(
"--channel-name",
type=str,
default="example_message_channel",
help="Optional channel display name",
)
parser.add_argument(
"--outbound-queue-capacity",
type=int,
default=256,
help="Bounded outbound queue length",
)
Comment on lines +55 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate outbound queue capacity as positive.

Non-positive values can silently produce unusable queue behavior for outbound messages.

💡 Suggested fix
+def _positive_int(value: str) -> int:
+    ivalue = int(value)
+    if ivalue <= 0:
+        raise argparse.ArgumentTypeError("must be > 0")
+    return ivalue
+
 def main() -> int:
@@
     parser.add_argument(
         "--outbound-queue-capacity",
-        type=int,
+        type=_positive_int,
         default=256,
         help="Bounded outbound queue length",
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/teleop_session_manager/python/message_channel_example.py` around
lines 55 - 60, The CLI option for --outbound-queue-capacity currently accepts
non-positive integers which lead to broken queue behavior; update parsing for
this flag (the parser.add_argument call for "--outbound-queue-capacity") to
validate that the value is > 0 — either by supplying a custom type/validator
that raises argparse.ArgumentTypeError for non-positive values or by checking
args.outbound_queue_capacity after parse_args and calling parser.error with a
clear message if it's <= 0; ensure the error message mentions
"--outbound-queue-capacity" so users know which flag is invalid.

args = parser.parse_args()

channel_uuid = _parse_uuid_bytes(args.channel_uuid)

source, sink = message_channel_config(
name="message_channel",
channel_uuid=channel_uuid,
channel_name=args.channel_name,
outbound_queue_capacity=args.outbound_queue_capacity,
)

config = TeleopSessionConfig(
app_name="MessageChannelExample",
pipeline=source,
)

print("=" * 80)
print("Message Channel TeleopSession Example")
print("=" * 80)
print(f"Channel UUID: {args.channel_uuid}")
print(f"Channel Name: {args.channel_name}")
print("Press Ctrl+C to exit.")
print()

send_counter = 0
last_send_time = 0.0

with TeleopSession(config) as session:
while True:
result = session.step()
status = result["status"][0]
messages_tracked = result["messages_tracked"][0]
messages = (
messages_tracked.data if messages_tracked.data is not None else []
)

for msg in messages:
payload = bytes(msg.payload)
try:
decoded = payload.decode("utf-8")
print(f"[rx] {decoded}")
except UnicodeDecodeError:
print(f"[rx] 0x{payload.hex()}")

now = time.monotonic()
if (
status == MessageChannelConnectionStatus.CONNECTED
and now - last_send_time >= 1.0
):
payload_text = f"hello #{send_counter} @ {time.time():.3f}"
_enqueue_outbound_message(sink, payload_text.encode("utf-8"))
print(f"[tx] {payload_text}")
last_send_time = now
send_counter += 1

time.sleep(0.01)

return 0


if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
print("\nExiting.")
sys.exit(0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include "tracker.hpp"

#include <cstdint>
#include <vector>

namespace core
{

struct MessageChannelMessagesT;
struct MessageChannelMessagesTrackedT;

enum class MessageChannelStatus : int32_t
{
CONNECTING = 0,
CONNECTED = 1,
SHUTTING = 2,
DISCONNECTED = 3,
UNKNOWN = -1,
};

class IMessageChannelTrackerImpl : public ITrackerImpl
{
public:
virtual MessageChannelStatus get_status() const = 0;
virtual const MessageChannelMessagesTrackedT& get_messages() const = 0;
virtual void send_message(const std::vector<uint8_t>& payload) const = 0;
};

} // namespace core
1 change: 1 addition & 0 deletions src/core/deviceio_trackers/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SPDX-License-Identifier: Apache-2.0
## No OpenXR dependency

- **`deviceio_trackers`** must **not** link **`OpenXR::headers`**, **`oxr::oxr_utils`**, or vendor extension targets, and must **not** `#include` OpenXR headers. Public API stays schema + **`deviceio_base`** only.
- This includes **`tracker_bindings.cpp`**: do not add `#include <openxr/openxr.h>` or any `XR_NV_*` extension headers here, even when the bound tracker wraps an OpenXR concept. The UUID is `std::array<uint8_t, 16>` at the `deviceio_trackers` boundary—no OpenXR types leak through.

## Related docs

Expand Down
2 changes: 2 additions & 0 deletions src/core/deviceio_trackers/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ add_library(deviceio_trackers STATIC
hand_tracker.cpp
head_tracker.cpp
controller_tracker.cpp
message_channel_tracker.cpp
generic_3axis_pedal_tracker.cpp
frame_metadata_tracker_oak.cpp
full_body_tracker_pico.cpp
inc/deviceio_trackers/head_tracker.hpp
inc/deviceio_trackers/hand_tracker.hpp
inc/deviceio_trackers/controller_tracker.hpp
inc/deviceio_trackers/message_channel_tracker.hpp
inc/deviceio_trackers/full_body_tracker_pico.hpp
inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp
inc/deviceio_trackers/frame_metadata_tracker_oak.hpp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <deviceio_base/message_channel_tracker_base.hpp>
#include <schema/message_channel_generated.h>

#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>

namespace core
{

class MessageChannelTracker : public ITracker
{
public:
static constexpr size_t DEFAULT_MAX_MESSAGE_SIZE = 64 * 1024;
static constexpr size_t CHANNEL_UUID_SIZE = 16;

explicit MessageChannelTracker(const std::array<uint8_t, CHANNEL_UUID_SIZE>& channel_uuid,
const std::string& channel_name = "",
size_t max_message_size = DEFAULT_MAX_MESSAGE_SIZE);

std::string_view get_name() const override
{
return TRACKER_NAME;
}

MessageChannelStatus get_status(const ITrackerSession& session) const;
const MessageChannelMessagesTrackedT& get_messages(const ITrackerSession& session) const;
void send_message(const ITrackerSession& session, const std::vector<uint8_t>& payload) const;

const std::array<uint8_t, CHANNEL_UUID_SIZE>& channel_uuid() const
{
return channel_uuid_;
}

const std::string& channel_name() const
{
return channel_name_;
}

size_t max_message_size() const
{
return max_message_size_;
}

private:
static constexpr const char* TRACKER_NAME = "MessageChannelTracker";

std::array<uint8_t, CHANNEL_UUID_SIZE> channel_uuid_{};
std::string channel_name_;
size_t max_message_size_{ DEFAULT_MAX_MESSAGE_SIZE };
};

} // namespace core
37 changes: 37 additions & 0 deletions src/core/deviceio_trackers/cpp/message_channel_tracker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include "inc/deviceio_trackers/message_channel_tracker.hpp"

#include <stdexcept>

namespace core
{

MessageChannelTracker::MessageChannelTracker(const std::array<uint8_t, CHANNEL_UUID_SIZE>& channel_uuid,
const std::string& channel_name,
size_t max_message_size)
: channel_uuid_(channel_uuid), channel_name_(channel_name), max_message_size_(max_message_size)
{
if (max_message_size_ == 0)
{
throw std::invalid_argument("MessageChannelTracker: max_message_size must be > 0");
}
}

MessageChannelStatus MessageChannelTracker::get_status(const ITrackerSession& session) const
{
return static_cast<const IMessageChannelTrackerImpl&>(session.get_tracker_impl(*this)).get_status();
}

const MessageChannelMessagesTrackedT& MessageChannelTracker::get_messages(const ITrackerSession& session) const
{
return static_cast<const IMessageChannelTrackerImpl&>(session.get_tracker_impl(*this)).get_messages();
}

void MessageChannelTracker::send_message(const ITrackerSession& session, const std::vector<uint8_t>& payload) const
{
static_cast<const IMessageChannelTrackerImpl&>(session.get_tracker_impl(*this)).send_message(payload);
}

} // namespace core
4 changes: 4 additions & 0 deletions src/core/deviceio_trackers/python/deviceio_trackers_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
HandTracker,
HeadTracker,
ControllerTracker,
MessageChannelStatus,
MessageChannelTracker,
FrameMetadataTrackerOak,
Generic3AxisPedalTracker,
FullBodyTrackerPico,
Expand All @@ -21,6 +23,8 @@

__all__ = [
"ControllerTracker",
"MessageChannelStatus",
"MessageChannelTracker",
"FrameMetadataTrackerOak",
"FullBodyTrackerPico",
"Generic3AxisPedalTracker",
Expand Down
Loading
Loading