Skip to content

Commit b908766

Browse files
committed
Add message channel tracker
1 parent 9d24cd1 commit b908766

28 files changed

Lines changed: 1444 additions & 0 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
/*!
4+
* @file
5+
* @brief Header for XR_NV_opaque_data_channel extension.
6+
*/
7+
#ifndef XR_NV_OPAQUE_DATA_CHANNEL_H
8+
#define XR_NV_OPAQUE_DATA_CHANNEL_H 1
9+
10+
#include "openxr_extension_helpers.h"
11+
12+
#ifdef __cplusplus
13+
extern "C" {
14+
#endif
15+
16+
#define XR_NV_opaque_data_channel 1
17+
#define XR_NV_opaque_data_channel_SPEC_VERSION 1
18+
#define XR_NV_OPAQUE_DATA_CHANNEL_EXTENSION_NAME "XR_NV_opaque_data_channel"
19+
20+
XR_DEFINE_HANDLE(XrOpaqueDataChannelNV)
21+
22+
XR_STRUCT_ENUM(XR_TYPE_OPAQUE_DATA_CHANNEL_CREATE_INFO_NV, 1000526001);
23+
XR_STRUCT_ENUM(XR_TYPE_OPAQUE_DATA_CHANNEL_STATE_NV, 1000526002);
24+
25+
XR_RESULT_ENUM(XR_ERROR_CHANNEL_ALREADY_CREATED_NV, -1000526000);
26+
XR_RESULT_ENUM(XR_ERROR_CHANNEL_NOT_CONNECTED_NV, -1000526001);
27+
28+
typedef enum XrOpaqueDataChannelStatusNV {
29+
XR_OPAQUE_DATA_CHANNEL_STATUS_CONNECTING_NV = 0,
30+
XR_OPAQUE_DATA_CHANNEL_STATUS_CONNECTED_NV = 1,
31+
XR_OPAQUE_DATA_CHANNEL_STATUS_SHUTTING_NV = 2,
32+
XR_OPAQUE_DATA_CHANNEL_STATUS_DISCONNECTED_NV = 3,
33+
XR_OPAQUE_DATA_CHANNEL_STATUS_MAX_ENUM = 0x7FFFFFFF,
34+
} XrOpaqueDataChannelStatusNV;
35+
36+
typedef struct XrOpaqueDataChannelCreateInfoNV {
37+
XrStructureType type;
38+
const void* next;
39+
XrSystemId systemId;
40+
XrUuidEXT uuid;
41+
} XrOpaqueDataChannelCreateInfoNV;
42+
43+
typedef struct XrOpaqueDataChannelStateNV {
44+
XrStructureType type;
45+
void* next;
46+
XrOpaqueDataChannelStatusNV state;
47+
} XrOpaqueDataChannelStateNV;
48+
49+
typedef XrResult(XRAPI_PTR* PFN_xrCreateOpaqueDataChannelNV)(XrInstance instance,
50+
const XrOpaqueDataChannelCreateInfoNV* createInfo,
51+
XrOpaqueDataChannelNV* opaqueDataChannel);
52+
typedef XrResult(XRAPI_PTR* PFN_xrDestroyOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel);
53+
typedef XrResult(XRAPI_PTR* PFN_xrGetOpaqueDataChannelStateNV)(XrOpaqueDataChannelNV opaqueDataChannel,
54+
XrOpaqueDataChannelStateNV* state);
55+
typedef XrResult(XRAPI_PTR* PFN_xrSendOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel,
56+
uint32_t opaqueDataInputCount,
57+
const uint8_t* opaqueDatas);
58+
typedef XrResult(XRAPI_PTR* PFN_xrReceiveOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel,
59+
uint32_t opaqueDataCapacityInput,
60+
uint32_t* opaqueDataCountOutput,
61+
uint8_t* opaqueDatas);
62+
typedef XrResult(XRAPI_PTR* PFN_xrShutdownOpaqueDataChannelNV)(XrOpaqueDataChannelNV opaqueDataChannel);
63+
64+
#ifdef __cplusplus
65+
}
66+
#endif
67+
68+
#endif
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""
6+
Message channel example using TeleopSession + retargeting source/sink nodes.
7+
8+
Behavior:
9+
- Prints any incoming messages each frame.
10+
- Once channel status is CONNECTED, sends one message every second.
11+
"""
12+
13+
import argparse
14+
import sys
15+
import time
16+
import uuid
17+
18+
from isaacteleop.retargeting_engine.deviceio_source_nodes import (
19+
MessageChannelConnectionStatus,
20+
message_channel_config,
21+
)
22+
from isaacteleop.retargeting_engine.interface import TensorGroup
23+
from isaacteleop.schema import MessageChannelMessages, MessageChannelMessagesTrackedT
24+
from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig
25+
26+
27+
def _parse_uuid_bytes(uuid_text: str) -> bytes:
28+
"""Parse canonical UUID text to 16-byte payload (argparse type= callable)."""
29+
try:
30+
return uuid.UUID(uuid_text).bytes
31+
except ValueError:
32+
raise argparse.ArgumentTypeError(
33+
f"--channel-uuid: invalid UUID {uuid_text!r} (expected canonical form, "
34+
"e.g. 550e8400-e29b-41d4-a716-446655440000)"
35+
)
36+
37+
38+
def _enqueue_outbound_message(sink, payload: bytes) -> None:
39+
"""Push one outbound message through MessageChannelSink."""
40+
tg = TensorGroup(sink.input_spec()["messages_tracked"])
41+
tg[0] = MessageChannelMessagesTrackedT([MessageChannelMessages(payload)])
42+
sink.compute({"messages_tracked": tg}, {})
43+
44+
45+
def main() -> int:
46+
parser = argparse.ArgumentParser(
47+
description="Message channel TeleopSession example"
48+
)
49+
parser.add_argument(
50+
"--channel-uuid",
51+
type=_parse_uuid_bytes,
52+
required=True,
53+
help="Message channel UUID (canonical form, e.g. 550e8400-e29b-41d4-a716-446655440000)",
54+
)
55+
parser.add_argument(
56+
"--channel-name",
57+
type=str,
58+
default="example_message_channel",
59+
help="Optional channel display name",
60+
)
61+
parser.add_argument(
62+
"--outbound-queue-capacity",
63+
type=int,
64+
default=256,
65+
help="Bounded outbound queue length",
66+
)
67+
args = parser.parse_args()
68+
69+
source, sink = message_channel_config(
70+
name="message_channel",
71+
channel_uuid=args.channel_uuid,
72+
channel_name=args.channel_name,
73+
outbound_queue_capacity=args.outbound_queue_capacity,
74+
)
75+
76+
config = TeleopSessionConfig(
77+
app_name="MessageChannelExample",
78+
pipeline=source,
79+
)
80+
81+
print("=" * 80)
82+
print("Message Channel TeleopSession Example")
83+
print("=" * 80)
84+
print(f"Channel UUID: {args.channel_uuid}")
85+
print(f"Channel Name: {args.channel_name}")
86+
print("Press Ctrl+C to exit.")
87+
print()
88+
89+
send_counter = 0
90+
last_send_time = 0.0
91+
92+
with TeleopSession(config) as session:
93+
while True:
94+
result = session.step()
95+
status = result["status"][0]
96+
messages_tracked = result["messages_tracked"][0]
97+
messages = (
98+
messages_tracked.data if messages_tracked.data is not None else []
99+
)
100+
101+
for msg in messages:
102+
payload = bytes(msg.payload)
103+
try:
104+
decoded = payload.decode("utf-8")
105+
print(f"[rx] {decoded}")
106+
except UnicodeDecodeError:
107+
print(f"[rx] 0x{payload.hex()}")
108+
109+
now = time.monotonic()
110+
if (
111+
status == MessageChannelConnectionStatus.CONNECTED
112+
and now - last_send_time >= 1.0
113+
):
114+
payload_text = f"hello #{send_counter} @ {time.time():.3f}"
115+
_enqueue_outbound_message(sink, payload_text.encode("utf-8"))
116+
print(f"[tx] {payload_text}")
117+
last_send_time = now
118+
send_counter += 1
119+
120+
time.sleep(0.01)
121+
122+
return 0
123+
124+
125+
if __name__ == "__main__":
126+
try:
127+
sys.exit(main())
128+
except KeyboardInterrupt:
129+
print("\nExiting.")
130+
sys.exit(0)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include "tracker.hpp"
7+
8+
#include <cstdint>
9+
#include <vector>
10+
11+
namespace core
12+
{
13+
14+
struct MessageChannelMessagesT;
15+
struct MessageChannelMessagesTrackedT;
16+
17+
enum class MessageChannelStatus : int32_t
18+
{
19+
CONNECTING = 0,
20+
CONNECTED = 1,
21+
SHUTTING = 2,
22+
DISCONNECTED = 3,
23+
UNKNOWN = -1,
24+
};
25+
26+
class IMessageChannelTrackerImpl : public ITrackerImpl
27+
{
28+
public:
29+
virtual MessageChannelStatus get_status() const = 0;
30+
virtual const MessageChannelMessagesTrackedT& get_messages() const = 0;
31+
virtual void send_message(const std::vector<uint8_t>& payload) const = 0;
32+
};
33+
34+
} // namespace core

src/core/deviceio_trackers/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ SPDX-License-Identifier: Apache-2.0
1010
## No OpenXR dependency
1111

1212
- **`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.
13+
- 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.
1314

1415
## Related docs
1516

src/core/deviceio_trackers/cpp/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ add_library(deviceio_trackers STATIC
88
hand_tracker.cpp
99
head_tracker.cpp
1010
controller_tracker.cpp
11+
message_channel_tracker.cpp
1112
generic_3axis_pedal_tracker.cpp
1213
frame_metadata_tracker_oak.cpp
1314
full_body_tracker_pico.cpp
1415
inc/deviceio_trackers/head_tracker.hpp
1516
inc/deviceio_trackers/hand_tracker.hpp
1617
inc/deviceio_trackers/controller_tracker.hpp
18+
inc/deviceio_trackers/message_channel_tracker.hpp
1719
inc/deviceio_trackers/full_body_tracker_pico.hpp
1820
inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp
1921
inc/deviceio_trackers/frame_metadata_tracker_oak.hpp
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include <deviceio_base/message_channel_tracker_base.hpp>
7+
#include <schema/message_channel_generated.h>
8+
9+
#include <array>
10+
#include <cstddef>
11+
#include <cstdint>
12+
#include <memory>
13+
#include <string>
14+
#include <vector>
15+
16+
namespace core
17+
{
18+
19+
class MessageChannelTracker : public ITracker
20+
{
21+
public:
22+
static constexpr size_t DEFAULT_MAX_MESSAGE_SIZE = 64 * 1024;
23+
static constexpr size_t CHANNEL_UUID_SIZE = 16;
24+
25+
explicit MessageChannelTracker(const std::array<uint8_t, CHANNEL_UUID_SIZE>& channel_uuid,
26+
const std::string& channel_name = "",
27+
size_t max_message_size = DEFAULT_MAX_MESSAGE_SIZE);
28+
29+
std::string_view get_name() const override
30+
{
31+
return TRACKER_NAME;
32+
}
33+
34+
MessageChannelStatus get_status(const ITrackerSession& session) const;
35+
const MessageChannelMessagesTrackedT& get_messages(const ITrackerSession& session) const;
36+
void send_message(const ITrackerSession& session, const std::vector<uint8_t>& payload) const;
37+
38+
const std::array<uint8_t, CHANNEL_UUID_SIZE>& channel_uuid() const
39+
{
40+
return channel_uuid_;
41+
}
42+
43+
const std::string& channel_name() const
44+
{
45+
return channel_name_;
46+
}
47+
48+
size_t max_message_size() const
49+
{
50+
return max_message_size_;
51+
}
52+
53+
private:
54+
static constexpr const char* TRACKER_NAME = "MessageChannelTracker";
55+
56+
std::array<uint8_t, CHANNEL_UUID_SIZE> channel_uuid_{};
57+
std::string channel_name_;
58+
size_t max_message_size_{ DEFAULT_MAX_MESSAGE_SIZE };
59+
};
60+
61+
} // namespace core
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#include "inc/deviceio_trackers/message_channel_tracker.hpp"
5+
6+
#include <stdexcept>
7+
8+
namespace core
9+
{
10+
11+
MessageChannelTracker::MessageChannelTracker(const std::array<uint8_t, CHANNEL_UUID_SIZE>& channel_uuid,
12+
const std::string& channel_name,
13+
size_t max_message_size)
14+
: channel_uuid_(channel_uuid), channel_name_(channel_name), max_message_size_(max_message_size)
15+
{
16+
if (max_message_size_ == 0)
17+
{
18+
throw std::invalid_argument("MessageChannelTracker: max_message_size must be > 0");
19+
}
20+
}
21+
22+
MessageChannelStatus MessageChannelTracker::get_status(const ITrackerSession& session) const
23+
{
24+
return static_cast<const IMessageChannelTrackerImpl&>(session.get_tracker_impl(*this)).get_status();
25+
}
26+
27+
const MessageChannelMessagesTrackedT& MessageChannelTracker::get_messages(const ITrackerSession& session) const
28+
{
29+
return static_cast<const IMessageChannelTrackerImpl&>(session.get_tracker_impl(*this)).get_messages();
30+
}
31+
32+
void MessageChannelTracker::send_message(const ITrackerSession& session, const std::vector<uint8_t>& payload) const
33+
{
34+
static_cast<const IMessageChannelTrackerImpl&>(session.get_tracker_impl(*this)).send_message(payload);
35+
}
36+
37+
} // namespace core

src/core/deviceio_trackers/python/deviceio_trackers_init.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
HandTracker,
99
HeadTracker,
1010
ControllerTracker,
11+
MessageChannelStatus,
12+
MessageChannelTracker,
1113
FrameMetadataTrackerOak,
1214
Generic3AxisPedalTracker,
1315
FullBodyTrackerPico,
@@ -21,6 +23,8 @@
2123

2224
__all__ = [
2325
"ControllerTracker",
26+
"MessageChannelStatus",
27+
"MessageChannelTracker",
2428
"FrameMetadataTrackerOak",
2529
"FullBodyTrackerPico",
2630
"Generic3AxisPedalTracker",

0 commit comments

Comments
 (0)