From 96e769f638ad70241902b38b1c88733431eb6f59 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Sun, 19 Jul 2026 23:37:11 +0000 Subject: [PATCH 1/5] Add C++ full-body tracking examples - examples/schemaio/full_body_printer.cpp: minimal C++ reader for FullBodyTrackerPico (XR_BD_body_tracking, 24 joints), following the se3_printer pattern. - examples/mcap_record_replay/cpp/record_full_body.cpp: C++ counterpart of record_full_body.py; records the full_body channel by passing a McapRecordingConfig to DeviceIOSession::run(), producing files that replay unchanged with replay_full_body.py. - Reference both examples from the body-tracking, trackers, and MCAP record/replay docs. Signed-off-by: Jiwen Cai --- docs/source/device/body_tracking.rst | 11 ++ docs/source/device/trackers.rst | 7 + docs/source/references/mcap_record_replay.rst | 7 + examples/mcap_record_replay/CMakeLists.txt | 3 + .../mcap_record_replay/cpp/CMakeLists.txt | 25 +++ .../cpp/record_full_body.cpp | 148 ++++++++++++++++++ examples/schemaio/CMakeLists.txt | 16 +- examples/schemaio/full_body_printer.cpp | 120 ++++++++++++++ 8 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 examples/mcap_record_replay/cpp/CMakeLists.txt create mode 100644 examples/mcap_record_replay/cpp/record_full_body.cpp create mode 100644 examples/schemaio/full_body_printer.cpp diff --git a/docs/source/device/body_tracking.rst b/docs/source/device/body_tracking.rst index 26de8d0a0..a3917df25 100644 --- a/docs/source/device/body_tracking.rst +++ b/docs/source/device/body_tracking.rst @@ -193,6 +193,11 @@ The ``all_joint_poses_tracked`` quality flag indicates whether every joint was successfully tracked in the current frame. When it is false, consult individual joint ``is_valid`` flags to determine which joints have valid poses. +For a minimal C++ reader see ``examples/schemaio/full_body_printer.cpp``, which +creates the tracker, queries the required OpenXR extensions, and prints the +joint data each frame through ``DeviceIOSession``. The Python equivalent is +``examples/oxr/python/test_full_body_tracker.py``. + Troubleshooting ~~~~~~~~~~~~~~~ @@ -217,6 +222,12 @@ same retargeting pipeline — no headset required during replay. See :doc:`../references/mcap_record_replay` for the recording / replay API and the ``record_full_body.py`` / ``replay_full_body.py`` example. +Recording is also available directly from C++: pass a ``McapRecordingConfig`` +to ``DeviceIOSession::run()`` with the tracker mapped to the ``full_body`` +channel base name. ``examples/mcap_record_replay/cpp/record_full_body.cpp`` +demonstrates this — a file recorded there replays unchanged with +``replay_full_body.py``. + .. figure:: ../_static/full-body-replay.gif :alt: Full body skeleton replayed from an MCAP recording in viser :class: no-image-zoom diff --git a/docs/source/device/trackers.rst b/docs/source/device/trackers.rst index 7bcac1bef..3334ff0f0 100644 --- a/docs/source/device/trackers.rst +++ b/docs/source/device/trackers.rst @@ -205,6 +205,13 @@ reads the PICO ``XR_BD_body_tracking`` extension directly. - :code-file:`src/core/schema_tests/python/test_full_body.py` - :code-file:`examples/oxr/python/test_full_body_tracker.py` +- Examples: + + - :code-file:`examples/schemaio/full_body_printer.cpp` + - :code-file:`examples/mcap_record_replay/cpp/record_full_body.cpp` + - :code-file:`examples/mcap_record_replay/python/record_full_body.py` + - :code-file:`examples/mcap_record_replay/python/replay_full_body.py` + .. note:: ``FullBodyTrackerPico`` remains available as a deprecated alias for diff --git a/docs/source/references/mcap_record_replay.rst b/docs/source/references/mcap_record_replay.rst index 9ed4d2edf..31d30fa6f 100644 --- a/docs/source/references/mcap_record_replay.rst +++ b/docs/source/references/mcap_record_replay.rst @@ -133,6 +133,13 @@ additional source nodes (``HeadSource``, ``ControllersSource``, …) in For a live browser view of **all** human DeviceIO trackers at once (hands, head, controllers, and full body), see ``examples/deviceio_live_view/python/``. +A C++ recorder lives at ``examples/mcap_record_replay/cpp/``: + +- ``record_full_body.cpp`` — records the ``full_body`` channel by passing a + ``core::McapRecordingConfig`` to ``DeviceIOSession::run()``. It uses the + same channel base name as ``FullBodySource``, so the resulting file replays + with ``replay_full_body.py``. + Recording ^^^^^^^^^ diff --git a/examples/mcap_record_replay/CMakeLists.txt b/examples/mcap_record_replay/CMakeLists.txt index 682432fef..7a977c5e2 100644 --- a/examples/mcap_record_replay/CMakeLists.txt +++ b/examples/mcap_record_replay/CMakeLists.txt @@ -3,5 +3,8 @@ cmake_minimum_required(VERSION 3.20) +# Build C++ examples +add_subdirectory(cpp) + include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake) install_python_example(DESTINATION examples/mcap_record_replay/python) diff --git a/examples/mcap_record_replay/cpp/CMakeLists.txt b/examples/mcap_record_replay/cpp/CMakeLists.txt new file mode 100644 index 000000000..fa22fc590 --- /dev/null +++ b/examples/mcap_record_replay/cpp/CMakeLists.txt @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20) + +find_package(Threads REQUIRED) + +# Full body MCAP recorder example (C++ counterpart of record_full_body.py) +add_executable(record_full_body + record_full_body.cpp +) + +target_link_libraries(record_full_body PRIVATE + deviceio::deviceio_session + deviceio::deviceio_trackers + oxr::oxr_core + isaacteleop_schema + ${CMAKE_DL_LIBS} + Threads::Threads +) + +# Install +install(TARGETS record_full_body + RUNTIME DESTINATION examples/mcap_record_replay/cpp +) diff --git a/examples/mcap_record_replay/cpp/record_full_body.cpp b/examples/mcap_record_replay/cpp/record_full_body.cpp new file mode 100644 index 000000000..7c63c698c --- /dev/null +++ b/examples/mcap_record_replay/cpp/record_full_body.cpp @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/*! + * @file record_full_body.cpp + * @brief Record a live full-body tracking session to an MCAP file using only the C++ API. + * + * C++ counterpart of record_full_body.py. Creates a FullBodyTrackerPico, opens an OpenXR + * session with its required extensions, and passes a McapRecordingConfig to + * DeviceIOSession::run() — the tracker impl then writes MCAP samples during each + * session->update() call. Unlike the Python example this does not launch the CloudXR runtime; + * start it (and connect the headset) before running. + * + * Usage: + * record_full_body [duration_seconds] [output.mcap] + * + * Defaults: 5 seconds -> full_body_.mcap in the current directory. + * + * The recording uses the same "full_body" channel base name as FullBodySource in the Python + * examples, so the file replays unchanged with + * examples/mcap_record_replay/python/replay_full_body.py. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +// Channel base name matching FullBodySource(name="full_body") in the Python examples. MCAP +// topics become "/", so this must be "full_body" for the recording to +// be readable by replay_full_body.py. +constexpr const char* MCAP_BASE_NAME = "full_body"; + +std::string default_output_path() +{ + const std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm tm_buf{}; + localtime_r(&now, &tm_buf); + std::ostringstream name; + name << "full_body_" << std::put_time(&tm_buf, "%Y%m%d_%H%M%S") << ".mcap"; + return name.str(); +} + +uint32_t count_valid_joints(const core::FullBodyPosePicoT& data) +{ + uint32_t valid_count = 0; + for (uint32_t i = 0; i < core::FullBodyTrackerPico::JOINT_COUNT; ++i) + { + if ((*data.joints->joints())[i]->is_valid()) + { + ++valid_count; + } + } + return valid_count; +} + +} // namespace + +int main(int argc, char** argv) +try +{ + const double duration_s = (argc > 1) ? std::stod(argv[1]) : 5.0; + const std::string mcap_path = (argc > 2) ? argv[2] : default_output_path(); + + std::cout << "[record] writing " << mcap_path << " for " << duration_s << "s" << std::endl; + + // Step 1: Create the tracker + auto tracker = std::make_shared(); + + // Step 2: Get required extensions and create OpenXR session + std::vector> trackers = { tracker }; + auto required_extensions = core::DeviceIOSession::get_required_extensions(trackers); + + auto oxr_session = std::make_shared("McapFullBodyRecordExample", required_extensions); + + // Step 3: Create DeviceIOSession with recording enabled + core::McapRecordingConfig recording_config{ mcap_path, { { tracker.get(), MCAP_BASE_NAME } } }; + + auto session = core::DeviceIOSession::run(trackers, oxr_session->get_handles(), std::move(recording_config)); + + // Step 4: Update the session for the requested duration. The tracker impl writes the MCAP + // sample during each update(); no explicit write call is needed here. + const auto start = std::chrono::steady_clock::now(); + size_t frame_count = 0; + while (true) + { + const double elapsed_s = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + if (elapsed_s >= duration_s) + { + break; + } + + session->update(); + + if (frame_count % 60 == 0) + { + const auto& tracked = tracker->get_body_pose(*session); + std::cout << "[record] t=" << std::fixed << std::setprecision(2) << elapsed_s << "s frame=" << frame_count; + if (tracked.data) + { + std::cout << " joints=" << count_valid_joints(*tracked.data) << "/" + << core::FullBodyTrackerPico::JOINT_COUNT; + } + else + { + std::cout << " [body tracking inactive]"; + } + std::cout << std::endl; + } + ++frame_count; + + // Tick at ~60 Hz, matching record_full_body.py. + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + + // Destroying the session closes the MCAP writer and emits the summary/statistics block that + // replay tooling (e.g. replay_full_body.py) relies on — exit normally rather than aborting. + session.reset(); + + std::cout << "[record] done — " << mcap_path << std::endl; + std::cout << "[record] replay with: python examples/mcap_record_replay/python/replay_full_body.py " << mcap_path + << std::endl; + return 0; +} +catch (const std::exception& e) +{ + std::cerr << argv[0] << ": " << e.what() << std::endl; + return 1; +} +catch (...) +{ + std::cerr << argv[0] << ": Unknown error occurred" << std::endl; + return 1; +} diff --git a/examples/schemaio/CMakeLists.txt b/examples/schemaio/CMakeLists.txt index cbd140f2d..422e51285 100644 --- a/examples/schemaio/CMakeLists.txt +++ b/examples/schemaio/CMakeLists.txt @@ -47,6 +47,20 @@ target_link_libraries(se3_printer PRIVATE Threads::Threads ) +# Create full body tracker printer executable +add_executable(full_body_printer + full_body_printer.cpp +) + +target_link_libraries(full_body_printer PRIVATE + deviceio::deviceio_session + deviceio::deviceio_trackers + oxr::oxr_core + isaacteleop_schema + ${CMAKE_DL_LIBS} + Threads::Threads +) + # Create frame metadata printer executable add_executable(frame_metadata_printer frame_metadata_printer.cpp @@ -62,6 +76,6 @@ target_link_libraries(frame_metadata_printer PRIVATE ) # Install -install(TARGETS pedal_pusher pedal_printer se3_printer frame_metadata_printer +install(TARGETS pedal_pusher pedal_printer se3_printer full_body_printer frame_metadata_printer RUNTIME DESTINATION examples/schemaio ) diff --git a/examples/schemaio/full_body_printer.cpp b/examples/schemaio/full_body_printer.cpp new file mode 100644 index 000000000..d18c4b1dd --- /dev/null +++ b/examples/schemaio/full_body_printer.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/*! + * @file full_body_printer.cpp + * @brief Standalone application that reads and prints PICO full-body poses from the OpenXR runtime. + * + * This application demonstrates using FullBodyTrackerPico (XR_BD_body_tracking) to read the + * 24-joint body skeleton through DeviceIOSession. Requires a runtime with body tracking support + * (e.g. CloudXR streaming from a PICO 4 Ultra Enterprise with Motion Trackers); when the system + * does not support body tracking the tracker runs in limp mode and no samples are printed. + * + * To record a full-body session to MCAP from C++, see + * examples/mcap_record_replay/cpp/record_full_body.cpp. + */ + +#include "common_utils.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace schemaio_example; + +namespace +{ + +void print_body_pose(const core::FullBodyPosePicoT& data, size_t sample_count) +{ + const auto& joints = *data.joints->joints(); + + uint32_t valid_count = 0; + for (uint32_t i = 0; i < core::FullBodyTrackerPico::JOINT_COUNT; ++i) + { + if (joints[i]->is_valid()) + { + ++valid_count; + } + } + + // Joint poses are unspecified while their tracking is lost — gate on is_valid per joint + // (all_joint_poses_tracked is a whole-skeleton quality flag only). + const auto& pelvis = joints[core::BodyJointPico_PELVIS]->pose().position(); + const auto& head = joints[core::BodyJointPico_HEAD]->pose().position(); + + std::cout << "Sample " << sample_count << std::fixed << std::setprecision(3) << " valid=" << valid_count << "/" + << core::FullBodyTrackerPico::JOINT_COUNT << " pelvis=[" << pelvis.x() << ", " << pelvis.y() << ", " + << pelvis.z() << "] head=[" << head.x() << ", " << head.y() << ", " << head.z() << "]" << std::endl; +} + +} // namespace + +int main(int argc, char** argv) +try +{ + std::cout << "Full Body Printer (XR_BD_body_tracking)" << std::endl; + + // Step 1: Create the tracker + std::cout << "[Step 1] Creating FullBodyTrackerPico..." << std::endl; + auto tracker = std::make_shared(); + + // Step 2: Get required extensions and create OpenXR session + std::cout << "[Step 2] Creating OpenXR session with required extensions..." << std::endl; + + std::vector> trackers = { tracker }; + auto required_extensions = core::DeviceIOSession::get_required_extensions(trackers); + + auto oxr_session = std::make_shared("FullBodyPrinter", required_extensions); + + std::cout << " OpenXR session created" << std::endl; + + // Step 3: Create DeviceIOSession with the tracker + std::cout << "[Step 3] Creating DeviceIOSession..." << std::endl; + + std::unique_ptr session; + session = core::DeviceIOSession::run(trackers, oxr_session->get_handles()); + + // Step 4: Read samples by updating the session + std::cout << "[Step 4] Reading samples..." << std::endl; + + size_t received_count = 0; + while (received_count < MAX_SAMPLES) + { + // Update session (this calls update on all trackers). + session->update(); + + // Print current data if available. tracked.data stays null while body tracking is + // inactive (limp mode or the runtime has not started delivering joints yet). + const auto& tracked = tracker->get_body_pose(*session); + if (tracked.data) + { + print_body_pose(*tracked.data, received_count++); + } + + // Tick at ~30 Hz. + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + + std::cout << "\nDone. Received " << received_count << " samples." << std::endl; + return 0; +} +catch (const std::exception& e) +{ + std::cerr << argv[0] << ": " << e.what() << std::endl; + return 1; +} +catch (...) +{ + std::cerr << argv[0] << ": Unknown error occurred" << std::endl; + return 1; +} From 6b1bf7d4fa62ceeef0d673ecdf0435177b52c110 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Mon, 20 Jul 2026 22:47:21 +0000 Subject: [PATCH 2/5] Add full_body rig: launch the full-body examples via isaacteleop.rig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the full-body tracking C++ examples into the rig launcher so one command brings up the whole session instead of a hand-started runtime: - rigs/full_body.yaml: first consumer-only rig — no producer plugin and no collection_id; the printer (validity gate) and the MCAP recorder read XR_BD_body_tracking joints directly from the runtime. No params: the recorder keeps its own defaults, and a fresh timestamped .mcap is written per Enter-rerun. - full_body_printer: print '[body tracking inactive]' once per second while body tracking is unavailable (limp mode), sharing the literal record_full_body already uses, so the rig's validity-gate pane is never silently mute. - rig_tests: load + footgun tests for the shipped rig, mirroring the se3_tracker pair. - docs: rig launch pointers in body_tracking.rst and mcap_record_replay.rst, a limp-mode troubleshooting entry, and rig.rst now documents the consumer-only rig shape (collection_id rendezvous scoped to producer rigs). Build green (full_body_printer, record_full_body), rig tests 30/30, pre-commit and Sphinx docs clean. Signed-off-by: Jiwen Cai --- docs/source/device/body_tracking.rst | 14 ++++++++-- docs/source/references/mcap_record_replay.rst | 4 ++- docs/source/references/rig.rst | 12 +++++--- .../cpp/record_full_body.cpp | 3 +- examples/schemaio/full_body_printer.cpp | 15 ++++++++-- rigs/full_body.yaml | 28 +++++++++++++++++++ src/core/rig_tests/python/test_config.py | 27 +++++++++++++++++- 7 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 rigs/full_body.yaml diff --git a/docs/source/device/body_tracking.rst b/docs/source/device/body_tracking.rst index a3917df25..eb31beac2 100644 --- a/docs/source/device/body_tracking.rst +++ b/docs/source/device/body_tracking.rst @@ -196,7 +196,10 @@ joint ``is_valid`` flags to determine which joints have valid poses. For a minimal C++ reader see ``examples/schemaio/full_body_printer.cpp``, which creates the tracker, queries the required OpenXR extensions, and prints the joint data each frame through ``DeviceIOSession``. The Python equivalent is -``examples/oxr/python/test_full_body_tracker.py``. +``examples/oxr/python/test_full_body_tracker.py``. Running +``python -m isaacteleop.rig rigs/full_body.yaml`` starts the CloudXR runtime, +this printer, and the C++ MCAP recorder together in one tmux session (see +:ref:`rig-launcher`). Troubleshooting ~~~~~~~~~~~~~~~ @@ -209,6 +212,11 @@ Troubleshooting enterprise features (see above). Verify all PICO motion trackers are paired, powered on, and calibrated. Confirm the PICO browser is up to date. +- **The printer prints** ``[body tracking inactive]``. The headset is + connected but body tracking is unavailable: check that the Motion Trackers + are paired and calibrated and that the headset has enterprise body-tracking + support. A printer that never reaches ``[Step 4] Reading samples...`` is + still waiting for the runtime / headset instead. - **Some joints report** ``is_valid: false``. The PICO runtime may temporarily lose tracking for individual joints during fast movement or partial occlusion. These joints will recover automatically once tracking is @@ -226,7 +234,9 @@ Recording is also available directly from C++: pass a ``McapRecordingConfig`` to ``DeviceIOSession::run()`` with the tracker mapped to the ``full_body`` channel base name. ``examples/mcap_record_replay/cpp/record_full_body.cpp`` demonstrates this — a file recorded there replays unchanged with -``replay_full_body.py``. +``replay_full_body.py``. The ``rigs/full_body.yaml`` rig includes a recorder +pane running this example; each :kbd:`Enter` rerun in that pane writes a fresh +timestamped take. .. figure:: ../_static/full-body-replay.gif :alt: Full body skeleton replayed from an MCAP recording in viser diff --git a/docs/source/references/mcap_record_replay.rst b/docs/source/references/mcap_record_replay.rst index 31d30fa6f..f72143099 100644 --- a/docs/source/references/mcap_record_replay.rst +++ b/docs/source/references/mcap_record_replay.rst @@ -138,7 +138,9 @@ A C++ recorder lives at ``examples/mcap_record_replay/cpp/``: - ``record_full_body.cpp`` — records the ``full_body`` channel by passing a ``core::McapRecordingConfig`` to ``DeviceIOSession::run()``. It uses the same channel base name as ``FullBodySource``, so the resulting file replays - with ``replay_full_body.py``. + with ``replay_full_body.py``. ``python -m isaacteleop.rig rigs/full_body.yaml`` + runs it together with the CloudXR runtime and the full-body printer (see + :ref:`rig-launcher`). Recording ^^^^^^^^^ diff --git a/docs/source/references/rig.rst b/docs/source/references/rig.rst index 80a1eb470..d574dcc37 100644 --- a/docs/source/references/rig.rst +++ b/docs/source/references/rig.rst @@ -119,6 +119,10 @@ own: - name: se3 printer (requires headset) command: "install/examples/schemaio/se3_printer {collection_id}" +``rigs/full_body.yaml`` shows the other supported shape — no ``producers`` +key, because its consumers read the tracking data directly from the runtime, +so there is no ``collection_id`` to rendezvous on. + Top-level keys: .. list-table:: @@ -163,10 +167,10 @@ errors — a typo fails loudly at load time instead of misbehaving in a pane. .. important:: - Producers and consumers rendezvous on a shared ``collection_id``, and a - mismatch is **silent no-data** by design. Define it once under ``params`` - and reference it as ``{collection_id}`` in every command — then one edit - in one place changes both sides together. + In a rig with both producers and consumers, the two sides rendezvous on a + shared ``collection_id``, and a mismatch is **silent no-data** by design. + Define it once under ``params`` and reference it as ``{collection_id}`` in + every command — then one edit in one place changes both sides together. .. warning:: diff --git a/examples/mcap_record_replay/cpp/record_full_body.cpp b/examples/mcap_record_replay/cpp/record_full_body.cpp index 7c63c698c..96b8d10ca 100644 --- a/examples/mcap_record_replay/cpp/record_full_body.cpp +++ b/examples/mcap_record_replay/cpp/record_full_body.cpp @@ -9,7 +9,8 @@ * session with its required extensions, and passes a McapRecordingConfig to * DeviceIOSession::run() — the tracker impl then writes MCAP samples during each * session->update() call. Unlike the Python example this does not launch the CloudXR runtime; - * start it (and connect the headset) before running. + * start it (and connect the headset) before running — or launch the runtime, printer, and + * recorder together with python -m isaacteleop.rig rigs/full_body.yaml. * * Usage: * record_full_body [duration_seconds] [output.mcap] diff --git a/examples/schemaio/full_body_printer.cpp b/examples/schemaio/full_body_printer.cpp index d18c4b1dd..8e8da2d01 100644 --- a/examples/schemaio/full_body_printer.cpp +++ b/examples/schemaio/full_body_printer.cpp @@ -9,6 +9,8 @@ * 24-joint body skeleton through DeviceIOSession. Requires a runtime with body tracking support * (e.g. CloudXR streaming from a PICO 4 Ultra Enterprise with Motion Trackers); when the system * does not support body tracking the tracker runs in limp mode and no samples are printed. + * Launch it together with the CloudXR runtime and the MCAP recorder via + * python -m isaacteleop.rig rigs/full_body.yaml. * * To record a full-body session to MCAP from C++, see * examples/mcap_record_replay/cpp/record_full_body.cpp. @@ -88,18 +90,27 @@ try std::cout << "[Step 4] Reading samples..." << std::endl; size_t received_count = 0; + size_t tick_count = 0; while (received_count < MAX_SAMPLES) { // Update session (this calls update on all trackers). session->update(); - // Print current data if available. tracked.data stays null while body tracking is - // inactive (limp mode or the runtime has not started delivering joints yet). + // Print current data if available. tracked.data is null only in limp mode (body tracking + // unsupported); a supported-but-untracked body still delivers data with valid=0/24 joints. const auto& tracked = tracker->get_body_pose(*session); if (tracked.data) { print_body_pose(*tracked.data, received_count++); } + else if (tick_count % 30 == 0) + { + // Heartbeat once per second (~30th tick at 30 Hz) so an inactive session is visible + // instead of silent. Same literal as record_full_body.cpp so both siblings report + // this state identically. + std::cout << "[body tracking inactive]" << std::endl; + } + ++tick_count; // Tick at ~30 Hz. std::this_thread::sleep_for(std::chrono::milliseconds(33)); diff --git a/rigs/full_body.yaml b/rigs/full_body.yaml new file mode 100644 index 000000000..3bba917fa --- /dev/null +++ b/rigs/full_body.yaml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Run with: python -m isaacteleop.rig rigs/full_body.yaml +# +# No producers: the two consumer panes below are C++ readers pulling +# XR_BD_body_tracking joints directly from the OpenXR runtime — no producer +# plugin, no collection_id. The launcher always adds the CloudXR runtime pane +# (no `runtime:` override needed), so the session is three panes: runtime + +# the two consumers below. Requires a runtime with body tracking (e.g. CloudXR +# streaming from a PICO 4 Ultra Enterprise with Motion Trackers). A pane that +# started before the headset connected exits with 'Failed to get OpenXR +# system' — press Enter to rerun. The printer prints '[body tracking inactive]' +# once per second when the headset is connected but body tracking is +# unavailable (calibration / enterprise support). Each recorder run writes a +# fresh full_body_.mcap into the repo root (optional args: +# [duration_seconds, default 5 s] [output.mcap]); exiting after ~5 s is +# SUCCESS — replay headless with +# examples/mcap_record_replay/python/replay_full_body.py . +# See rigs/se3_tracker.yaml for the fully annotated exemplar of every key. +name: full_body +description: CloudXR runtime + full-body joint printer + MCAP recorder (PICO 4 Ultra Enterprise with Motion Trackers) +cwd: .. # -> Teleop repo root +consumers: + - name: full body printer (requires headset + motion trackers) + command: "install/examples/schemaio/full_body_printer" + - name: full body recorder (check printer pane first; Enter = new timestamped take) + command: "install/examples/mcap_record_replay/cpp/record_full_body" diff --git a/src/core/rig_tests/python/test_config.py b/src/core/rig_tests/python/test_config.py index 6618ddcd6..14a699041 100644 --- a/src/core/rig_tests/python/test_config.py +++ b/src/core/rig_tests/python/test_config.py @@ -21,6 +21,7 @@ REPO_ROOT = Path(__file__).resolve().parents[4] SE3_RIG = REPO_ROOT / "rigs" / "se3_tracker.yaml" +FULL_BODY_RIG = REPO_ROOT / "rigs" / "full_body.yaml" def write_rig(tmp_path: Path, body: str) -> Path: @@ -38,7 +39,7 @@ def write_rig(tmp_path: Path, body: str) -> Path: # --------------------------------------------------------------------------- -# Loading the shipped exemplar +# Loading the shipped rigs # --------------------------------------------------------------------------- @@ -67,6 +68,30 @@ def test_se3_rig_has_no_footguns(): ) +def test_load_full_body_rig(): + config = load_rig_config(FULL_BODY_RIG) + assert config.name == "full_body" + assert config.description + assert config.cwd == REPO_ROOT # cwd: .. resolves against rigs/ + assert config.runtime is None + assert config.runtime_command == DEFAULT_RUNTIME_COMMAND + # Consumer-only rig: both panes read the runtime directly, so there are no + # producers, no params, and no collection_id to rendezvous on. + assert config.params == {} + assert len(config.producers) == 0 + assert len(config.consumers) == 2 + + +def test_full_body_rig_has_no_footguns(): + config = load_rig_config(FULL_BODY_RIG) + assert ( + find_runtime_footguns( + [*config.producers, *config.consumers], runtime_managed=True + ) + == [] + ) + + # --------------------------------------------------------------------------- # Validation errors (each a hard error naming the file) # --------------------------------------------------------------------------- From 654cf7d5e07a63406a12a270ac13b11657f96cd8 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Wed, 22 Jul 2026 17:36:35 +0000 Subject: [PATCH 3/5] Adopt vendor-agnostic FullBodyTracker API in the C++ full-body examples Follow-up to the generic FullBodyTracker refactor (#781), which removed deviceio_trackers/full_body_tracker_pico.hpp and renamed the Pico types: FullBodyTrackerPico -> FullBodyTracker, FullBodyPosePicoT -> FullBodyPoseT, BodyJointPico_* -> BodyJoint_*. full_body_printer and record_full_body now use the new header and names; no behavior change (the session still selects the default vendor). Both targets build, rig tests 30/30, clang-format-14 and pre-commit clean. Signed-off-by: Jiwen Cai --- .../cpp/record_full_body.cpp | 12 ++++++------ examples/schemaio/full_body_printer.cpp | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/mcap_record_replay/cpp/record_full_body.cpp b/examples/mcap_record_replay/cpp/record_full_body.cpp index 96b8d10ca..711faae63 100644 --- a/examples/mcap_record_replay/cpp/record_full_body.cpp +++ b/examples/mcap_record_replay/cpp/record_full_body.cpp @@ -5,7 +5,7 @@ * @file record_full_body.cpp * @brief Record a live full-body tracking session to an MCAP file using only the C++ API. * - * C++ counterpart of record_full_body.py. Creates a FullBodyTrackerPico, opens an OpenXR + * C++ counterpart of record_full_body.py. Creates a FullBodyTracker, opens an OpenXR * session with its required extensions, and passes a McapRecordingConfig to * DeviceIOSession::run() — the tracker impl then writes MCAP samples during each * session->update() call. Unlike the Python example this does not launch the CloudXR runtime; @@ -23,7 +23,7 @@ */ #include -#include +#include #include #include @@ -56,10 +56,10 @@ std::string default_output_path() return name.str(); } -uint32_t count_valid_joints(const core::FullBodyPosePicoT& data) +uint32_t count_valid_joints(const core::FullBodyPoseT& data) { uint32_t valid_count = 0; - for (uint32_t i = 0; i < core::FullBodyTrackerPico::JOINT_COUNT; ++i) + for (uint32_t i = 0; i < core::FullBodyTracker::JOINT_COUNT; ++i) { if ((*data.joints->joints())[i]->is_valid()) { @@ -80,7 +80,7 @@ try std::cout << "[record] writing " << mcap_path << " for " << duration_s << "s" << std::endl; // Step 1: Create the tracker - auto tracker = std::make_shared(); + auto tracker = std::make_shared(); // Step 2: Get required extensions and create OpenXR session std::vector> trackers = { tracker }; @@ -114,7 +114,7 @@ try if (tracked.data) { std::cout << " joints=" << count_valid_joints(*tracked.data) << "/" - << core::FullBodyTrackerPico::JOINT_COUNT; + << core::FullBodyTracker::JOINT_COUNT; } else { diff --git a/examples/schemaio/full_body_printer.cpp b/examples/schemaio/full_body_printer.cpp index 8e8da2d01..0151c46eb 100644 --- a/examples/schemaio/full_body_printer.cpp +++ b/examples/schemaio/full_body_printer.cpp @@ -5,7 +5,7 @@ * @file full_body_printer.cpp * @brief Standalone application that reads and prints PICO full-body poses from the OpenXR runtime. * - * This application demonstrates using FullBodyTrackerPico (XR_BD_body_tracking) to read the + * This application demonstrates using FullBodyTracker (XR_BD_body_tracking) to read the * 24-joint body skeleton through DeviceIOSession. Requires a runtime with body tracking support * (e.g. CloudXR streaming from a PICO 4 Ultra Enterprise with Motion Trackers); when the system * does not support body tracking the tracker runs in limp mode and no samples are printed. @@ -19,7 +19,7 @@ #include "common_utils.hpp" #include -#include +#include #include #include @@ -36,12 +36,12 @@ using namespace schemaio_example; namespace { -void print_body_pose(const core::FullBodyPosePicoT& data, size_t sample_count) +void print_body_pose(const core::FullBodyPoseT& data, size_t sample_count) { const auto& joints = *data.joints->joints(); uint32_t valid_count = 0; - for (uint32_t i = 0; i < core::FullBodyTrackerPico::JOINT_COUNT; ++i) + for (uint32_t i = 0; i < core::FullBodyTracker::JOINT_COUNT; ++i) { if (joints[i]->is_valid()) { @@ -51,11 +51,11 @@ void print_body_pose(const core::FullBodyPosePicoT& data, size_t sample_count) // Joint poses are unspecified while their tracking is lost — gate on is_valid per joint // (all_joint_poses_tracked is a whole-skeleton quality flag only). - const auto& pelvis = joints[core::BodyJointPico_PELVIS]->pose().position(); - const auto& head = joints[core::BodyJointPico_HEAD]->pose().position(); + const auto& pelvis = joints[core::BodyJoint_PELVIS]->pose().position(); + const auto& head = joints[core::BodyJoint_HEAD]->pose().position(); std::cout << "Sample " << sample_count << std::fixed << std::setprecision(3) << " valid=" << valid_count << "/" - << core::FullBodyTrackerPico::JOINT_COUNT << " pelvis=[" << pelvis.x() << ", " << pelvis.y() << ", " + << core::FullBodyTracker::JOINT_COUNT << " pelvis=[" << pelvis.x() << ", " << pelvis.y() << ", " << pelvis.z() << "] head=[" << head.x() << ", " << head.y() << ", " << head.z() << "]" << std::endl; } @@ -67,8 +67,8 @@ try std::cout << "Full Body Printer (XR_BD_body_tracking)" << std::endl; // Step 1: Create the tracker - std::cout << "[Step 1] Creating FullBodyTrackerPico..." << std::endl; - auto tracker = std::make_shared(); + std::cout << "[Step 1] Creating FullBodyTracker..." << std::endl; + auto tracker = std::make_shared(); // Step 2: Get required extensions and create OpenXR session std::cout << "[Step 2] Creating OpenXR session with required extensions..." << std::endl; From fd9ed8b30cba41b94cb7fac3b2fd106a81bc2980 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Wed, 22 Jul 2026 17:56:48 +0000 Subject: [PATCH 4/5] Address review feedback on the full-body C++ examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - full_body_printer: gate the pelvis/head printout on each joint's own is_valid flag ([not tracked] marker instead of unspecified coordinates), and bound the read loop (2x the sample budget) so limp mode cannot spin forever — a zero-sample run now exits with a clear message. - Keep the C++ examples self-contained: drop all references to the Python examples from sources, comments, and the recorder's stdout (the rig file pointer stays; the two-ways-to-read-full-body story lives in the docs). - Drop CMAKE_DL_LIBS from both example targets: dl is propagated transitively by the OpenXR loader; verified by linking without it. Signed-off-by: Jiwen Cai --- .../mcap_record_replay/cpp/CMakeLists.txt | 3 +- .../cpp/record_full_body.cpp | 28 ++++++------- examples/schemaio/CMakeLists.txt | 1 - examples/schemaio/full_body_printer.cpp | 40 ++++++++++++++----- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/examples/mcap_record_replay/cpp/CMakeLists.txt b/examples/mcap_record_replay/cpp/CMakeLists.txt index fa22fc590..18d72260d 100644 --- a/examples/mcap_record_replay/cpp/CMakeLists.txt +++ b/examples/mcap_record_replay/cpp/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.20) find_package(Threads REQUIRED) -# Full body MCAP recorder example (C++ counterpart of record_full_body.py) +# Full body MCAP recorder example add_executable(record_full_body record_full_body.cpp ) @@ -15,7 +15,6 @@ target_link_libraries(record_full_body PRIVATE deviceio::deviceio_trackers oxr::oxr_core isaacteleop_schema - ${CMAKE_DL_LIBS} Threads::Threads ) diff --git a/examples/mcap_record_replay/cpp/record_full_body.cpp b/examples/mcap_record_replay/cpp/record_full_body.cpp index 711faae63..6008c16a1 100644 --- a/examples/mcap_record_replay/cpp/record_full_body.cpp +++ b/examples/mcap_record_replay/cpp/record_full_body.cpp @@ -5,21 +5,19 @@ * @file record_full_body.cpp * @brief Record a live full-body tracking session to an MCAP file using only the C++ API. * - * C++ counterpart of record_full_body.py. Creates a FullBodyTracker, opens an OpenXR - * session with its required extensions, and passes a McapRecordingConfig to - * DeviceIOSession::run() — the tracker impl then writes MCAP samples during each - * session->update() call. Unlike the Python example this does not launch the CloudXR runtime; - * start it (and connect the headset) before running — or launch the runtime, printer, and - * recorder together with python -m isaacteleop.rig rigs/full_body.yaml. + * Creates a FullBodyTracker, opens an OpenXR session with its required extensions, and + * passes a McapRecordingConfig to DeviceIOSession::run() — the tracker impl then writes + * MCAP samples during each session->update() call. This application does not launch the + * CloudXR runtime; start it (and connect the headset) before running, or use the full_body + * rig (rigs/full_body.yaml) to launch the runtime, printer, and recorder together. * * Usage: * record_full_body [duration_seconds] [output.mcap] * * Defaults: 5 seconds -> full_body_.mcap in the current directory. * - * The recording uses the same "full_body" channel base name as FullBodySource in the Python - * examples, so the file replays unchanged with - * examples/mcap_record_replay/python/replay_full_body.py. + * The recording is written to the standard "full_body" channel, so any Isaac Teleop + * replay tooling can read it (see the MCAP record & replay documentation). */ #include @@ -41,9 +39,9 @@ namespace { -// Channel base name matching FullBodySource(name="full_body") in the Python examples. MCAP -// topics become "/", so this must be "full_body" for the recording to -// be readable by replay_full_body.py. +// Standard channel base name for full-body recordings. MCAP topics become +// "/", and replay tooling reads the "full_body" channel, so this +// must not change. constexpr const char* MCAP_BASE_NAME = "full_body"; std::string default_output_path() @@ -124,17 +122,15 @@ try } ++frame_count; - // Tick at ~60 Hz, matching record_full_body.py. + // Tick at ~60 Hz. std::this_thread::sleep_for(std::chrono::milliseconds(16)); } // Destroying the session closes the MCAP writer and emits the summary/statistics block that - // replay tooling (e.g. replay_full_body.py) relies on — exit normally rather than aborting. + // replay tooling relies on — exit normally rather than aborting. session.reset(); std::cout << "[record] done — " << mcap_path << std::endl; - std::cout << "[record] replay with: python examples/mcap_record_replay/python/replay_full_body.py " << mcap_path - << std::endl; return 0; } catch (const std::exception& e) diff --git a/examples/schemaio/CMakeLists.txt b/examples/schemaio/CMakeLists.txt index 422e51285..938f8e441 100644 --- a/examples/schemaio/CMakeLists.txt +++ b/examples/schemaio/CMakeLists.txt @@ -57,7 +57,6 @@ target_link_libraries(full_body_printer PRIVATE deviceio::deviceio_trackers oxr::oxr_core isaacteleop_schema - ${CMAKE_DL_LIBS} Threads::Threads ) diff --git a/examples/schemaio/full_body_printer.cpp b/examples/schemaio/full_body_printer.cpp index 0151c46eb..42f4a5db5 100644 --- a/examples/schemaio/full_body_printer.cpp +++ b/examples/schemaio/full_body_printer.cpp @@ -9,8 +9,8 @@ * 24-joint body skeleton through DeviceIOSession. Requires a runtime with body tracking support * (e.g. CloudXR streaming from a PICO 4 Ultra Enterprise with Motion Trackers); when the system * does not support body tracking the tracker runs in limp mode and no samples are printed. - * Launch it together with the CloudXR runtime and the MCAP recorder via - * python -m isaacteleop.rig rigs/full_body.yaml. + * The full_body rig (rigs/full_body.yaml) launches this printer together with the CloudXR + * runtime and the MCAP recorder in one tmux session. * * To record a full-body session to MCAP from C++, see * examples/mcap_record_replay/cpp/record_full_body.cpp. @@ -36,6 +36,21 @@ using namespace schemaio_example; namespace { +// Joint poses are unspecified while their tracking is lost — gate on is_valid per joint +// (all_joint_poses_tracked is a whole-skeleton quality flag only). +void print_joint(const char* label, const core::BodyJointPose& joint) +{ + if (joint.is_valid()) + { + const auto& position = joint.pose().position(); + std::cout << " " << label << "=[" << position.x() << ", " << position.y() << ", " << position.z() << "]"; + } + else + { + std::cout << " " << label << "=[not tracked]"; + } +} + void print_body_pose(const core::FullBodyPoseT& data, size_t sample_count) { const auto& joints = *data.joints->joints(); @@ -49,14 +64,11 @@ void print_body_pose(const core::FullBodyPoseT& data, size_t sample_count) } } - // Joint poses are unspecified while their tracking is lost — gate on is_valid per joint - // (all_joint_poses_tracked is a whole-skeleton quality flag only). - const auto& pelvis = joints[core::BodyJoint_PELVIS]->pose().position(); - const auto& head = joints[core::BodyJoint_HEAD]->pose().position(); - std::cout << "Sample " << sample_count << std::fixed << std::setprecision(3) << " valid=" << valid_count << "/" - << core::FullBodyTracker::JOINT_COUNT << " pelvis=[" << pelvis.x() << ", " << pelvis.y() << ", " - << pelvis.z() << "] head=[" << head.x() << ", " << head.y() << ", " << head.z() << "]" << std::endl; + << core::FullBodyTracker::JOINT_COUNT; + print_joint("pelvis", *joints[core::BodyJoint_PELVIS]); + print_joint("head", *joints[core::BodyJoint_HEAD]); + std::cout << std::endl; } } // namespace @@ -89,9 +101,13 @@ try // Step 4: Read samples by updating the session std::cout << "[Step 4] Reading samples..." << std::endl; + // Bound the loop so it cannot spin forever when no samples ever arrive (limp mode): a + // healthy session delivers a sample almost every tick, so twice the sample budget is ample. + constexpr size_t MAX_TICKS = 2 * MAX_SAMPLES; + size_t received_count = 0; size_t tick_count = 0; - while (received_count < MAX_SAMPLES) + while (received_count < MAX_SAMPLES && tick_count < MAX_TICKS) { // Update session (this calls update on all trackers). session->update(); @@ -116,6 +132,10 @@ try std::this_thread::sleep_for(std::chrono::milliseconds(33)); } + if (received_count == 0) + { + std::cout << "\nNo body tracking samples received — body tracking is inactive or unsupported." << std::endl; + } std::cout << "\nDone. Received " << received_count << " samples." << std::endl; return 0; } From 94af205ce59408dd35900c638c75ebe75adb7189 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Wed, 22 Jul 2026 18:02:29 +0000 Subject: [PATCH 5/5] Write rig full-body recordings into the shared recordings directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the full_body rig dropped full_body_.mcap into the repo root (the pane cwd), showing up as untracked noise in git status. - record_full_body: the output argument may now be a directory (existing or with a trailing slash) — it is created if needed and each run writes a fresh timestamped file inside it, so reruns still never clobber. - rigs/full_body.yaml: point the recorder at examples/mcap_record_replay/recordings/ — already gitignored and the default search location of the replay workflow, so a bare replay invocation picks up rig takes automatically. - body_tracking.rst: document where takes land. Verified all three output modes (dir arg creates the directory and timestamps inside; explicit .mcap path unchanged; bare default unchanged); rig tests 30/30; clang-format-14 and pre-commit clean. Signed-off-by: Jiwen Cai --- docs/source/device/body_tracking.rst | 3 ++- .../cpp/record_full_body.cpp | 22 ++++++++++++++++--- rigs/full_body.yaml | 11 +++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/source/device/body_tracking.rst b/docs/source/device/body_tracking.rst index eb31beac2..e247d86fc 100644 --- a/docs/source/device/body_tracking.rst +++ b/docs/source/device/body_tracking.rst @@ -236,7 +236,8 @@ channel base name. ``examples/mcap_record_replay/cpp/record_full_body.cpp`` demonstrates this — a file recorded there replays unchanged with ``replay_full_body.py``. The ``rigs/full_body.yaml`` rig includes a recorder pane running this example; each :kbd:`Enter` rerun in that pane writes a fresh -timestamped take. +timestamped take into ``examples/mcap_record_replay/recordings/``, where +``replay_full_body.py`` looks by default. .. figure:: ../_static/full-body-replay.gif :alt: Full body skeleton replayed from an MCAP recording in viser diff --git a/examples/mcap_record_replay/cpp/record_full_body.cpp b/examples/mcap_record_replay/cpp/record_full_body.cpp index 6008c16a1..b09c44038 100644 --- a/examples/mcap_record_replay/cpp/record_full_body.cpp +++ b/examples/mcap_record_replay/cpp/record_full_body.cpp @@ -12,9 +12,11 @@ * rig (rigs/full_body.yaml) to launch the runtime, printer, and recorder together. * * Usage: - * record_full_body [duration_seconds] [output.mcap] + * record_full_body [duration_seconds] [output.mcap | output_dir/] * - * Defaults: 5 seconds -> full_body_.mcap in the current directory. + * Defaults: 5 seconds -> full_body_.mcap in the current directory. When the + * output argument is a directory (existing, or with a trailing slash), it is created if + * needed and each run writes a fresh timestamped file inside it. * * The recording is written to the standard "full_body" channel, so any Isaac Teleop * replay tooling can read it (see the MCAP record & replay documentation). @@ -28,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +57,19 @@ std::string default_output_path() return name.str(); } +// An existing directory (or a path with a trailing slash) receives a timestamped file +// inside it — created if needed — so repeated runs never clobber a previous take. +std::string resolve_output_path(const std::string& arg) +{ + const std::filesystem::path path(arg); + if (std::filesystem::is_directory(path) || (!arg.empty() && arg.back() == '/')) + { + std::filesystem::create_directories(path); + return (path / default_output_path()).string(); + } + return arg; +} + uint32_t count_valid_joints(const core::FullBodyPoseT& data) { uint32_t valid_count = 0; @@ -73,7 +89,7 @@ int main(int argc, char** argv) try { const double duration_s = (argc > 1) ? std::stod(argv[1]) : 5.0; - const std::string mcap_path = (argc > 2) ? argv[2] : default_output_path(); + const std::string mcap_path = (argc > 2) ? resolve_output_path(argv[2]) : default_output_path(); std::cout << "[record] writing " << mcap_path << " for " << duration_s << "s" << std::endl; diff --git a/rigs/full_body.yaml b/rigs/full_body.yaml index 3bba917fa..ae33499d9 100644 --- a/rigs/full_body.yaml +++ b/rigs/full_body.yaml @@ -13,10 +13,11 @@ # system' — press Enter to rerun. The printer prints '[body tracking inactive]' # once per second when the headset is connected but body tracking is # unavailable (calibration / enterprise support). Each recorder run writes a -# fresh full_body_.mcap into the repo root (optional args: -# [duration_seconds, default 5 s] [output.mcap]); exiting after ~5 s is -# SUCCESS — replay headless with -# examples/mcap_record_replay/python/replay_full_body.py . +# fresh full_body_.mcap into examples/mcap_record_replay/recordings/ +# (gitignored; created if missing; recorder args: [duration_seconds, default +# 5 s] [output.mcap | output_dir/]); exiting after ~5 s is SUCCESS — replay +# headless with examples/mcap_record_replay/python/replay_full_body.py, which +# picks up the newest recording there by default. # See rigs/se3_tracker.yaml for the fully annotated exemplar of every key. name: full_body description: CloudXR runtime + full-body joint printer + MCAP recorder (PICO 4 Ultra Enterprise with Motion Trackers) @@ -25,4 +26,4 @@ consumers: - name: full body printer (requires headset + motion trackers) command: "install/examples/schemaio/full_body_printer" - name: full body recorder (check printer pane first; Enter = new timestamped take) - command: "install/examples/mcap_record_replay/cpp/record_full_body" + command: "install/examples/mcap_record_replay/cpp/record_full_body 5 examples/mcap_record_replay/recordings/"