-
Notifications
You must be signed in to change notification settings - Fork 87
Add spacemouse SE3/SE2 device: schema, tracker, plugin, retargeters #1019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rwiltz
wants to merge
8
commits into
main
Choose a base branch
from
rwiltz/implement-spacemouse
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b699afd
Add spacemouse SE3/SE2 device: schema, tracker, plugin, retargeters
rwiltz 119d1f6
Document Se2 omega_z sign asymmetry, add multi-button bitmap test
rwiltz 42f1249
Add spacemouse printer example
rwiltz 03dbd6f
Reset motion state and guard destructor against double-close on devic…
rwiltz 2816d62
Fix gripper spurious toggle when left button is held across a reset f…
rwiltz c132d66
Declare the retargeters-lite extra for SpaceMouseGripperRetargeter
rwiltz 1061f76
Preserve gripper edge state on reset frames with no spacemouse data
rwiltz 3b29a9f
Trim implementation notes from SpaceMouseOutputRecord schema comment
rwiltz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """ | ||
| SpaceMouse Printer Example. | ||
|
|
||
| Prints the translation axes, rotation axes, and every currently-held button each | ||
| frame, via SpaceMouseSource's "spacemouse_translation", "spacemouse_rotation", and | ||
| "spacemouse_buttons" outputs. Carries no semantic mapping (a position delta, a | ||
| rotation delta, a gripper toggle) -- that belongs in a retargeter (e.g. | ||
| SpaceMouseToSe3RelRetargeter) consuming this source's output. The spacemouse plugin | ||
| self-discovers its device and is auto-launched by TeleopSession -- no external | ||
| process to start manually. | ||
| """ | ||
|
|
||
| import sys | ||
| import time | ||
| from pathlib import Path | ||
|
|
||
| from isaacteleop.cloudxr import CloudXRLauncher | ||
| from isaacteleop.retargeting_engine.deviceio_source_nodes import SpaceMouseSource | ||
| from isaacteleop.teleop_session_manager import ( | ||
| TeleopSession, | ||
| TeleopSessionConfig, | ||
| PluginConfig, | ||
| ) | ||
|
|
||
|
|
||
| PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins" | ||
| PLUGIN_NAME = "spacemouse" | ||
| PLUGIN_ROOT_ID = "spacemouse" | ||
|
|
||
|
|
||
| def main(): | ||
| import argparse | ||
|
|
||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| CloudXRLauncher.add_launcher_arguments(parser) | ||
| args = parser.parse_args() | ||
|
|
||
| print("\n" + "=" * 80) | ||
| print(" SpaceMouse Printer Example") | ||
| print("=" * 80) | ||
| print("Move or twist the connected SpaceMouse, or press a button.") | ||
| print("=" * 80 + "\n") | ||
|
|
||
| # ================================================================== | ||
| # Setup: Create spacemouse source | ||
| # ================================================================== | ||
| spacemouse_source = SpaceMouseSource(name="spacemouse") | ||
|
|
||
| # ================================================================== | ||
| # Configure Plugins | ||
| # ================================================================== | ||
|
|
||
| plugins = [] | ||
| if PLUGIN_ROOT_DIR.exists(): | ||
| plugins.append( | ||
| PluginConfig( | ||
| plugin_name=PLUGIN_NAME, | ||
| plugin_root_id=PLUGIN_ROOT_ID, | ||
| search_paths=[PLUGIN_ROOT_DIR], | ||
| ) | ||
| ) | ||
|
|
||
| # ================================================================== | ||
| # Create and run TeleopSession | ||
| # ================================================================== | ||
|
|
||
| session_config = TeleopSessionConfig( | ||
| app_name="SpaceMousePrinterExample", | ||
| trackers=[], | ||
| pipeline=spacemouse_source, | ||
| plugins=plugins, | ||
| ) | ||
|
|
||
| with CloudXRLauncher.launch_context(args): | ||
| with TeleopSession(session_config) as session: | ||
| start_time = time.time() | ||
| prev_pressed: set[int] = set() | ||
|
|
||
| while time.time() - start_time < 30.0: | ||
| result = session.step() | ||
| translation_group = result["spacemouse_translation"] | ||
| rotation_group = result["spacemouse_rotation"] | ||
| buttons_group = result["spacemouse_buttons"] | ||
|
|
||
| elapsed = session.get_elapsed_time() | ||
| if translation_group.is_none: | ||
| print( | ||
| f"[{elapsed:5.1f}s] (no spacemouse data yet)", | ||
| end="\r", | ||
| flush=True, | ||
| ) | ||
| time.sleep(0.01) | ||
| continue | ||
|
|
||
| translation = translation_group[0] | ||
| rotation = rotation_group[0] | ||
| bitmap = buttons_group[0] | ||
| pressed = {code for code in range(len(bitmap)) if bitmap[code]} | ||
|
|
||
| t_str = " ".join(f"{v:+.2f}" for v in translation) | ||
| r_str = " ".join(f"{v:+.2f}" for v in rotation) | ||
|
|
||
| # Live status line (overwritten each frame). | ||
| names = [f"btn{code}" for code in sorted(pressed)] | ||
| print( | ||
| f"[{elapsed:5.1f}s] T: [{t_str}] R: [{r_str}] Held: {' '.join(names) or '-'}" | ||
| + " " * 20, | ||
| end="\r", | ||
| flush=True, | ||
| ) | ||
|
|
||
| # Permanent, scrollable log of every press/release transition -- a | ||
| # quick tap can flash by on the status line above before you notice | ||
| # it, but every transition is logged here. | ||
| for code in sorted(pressed - prev_pressed): | ||
| print(f"[{elapsed:5.1f}s] btn{code} down") | ||
| for code in sorted(prev_pressed - pressed): | ||
| print(f"[{elapsed:5.1f}s] btn{code} up") | ||
| prev_pressed = pressed | ||
|
|
||
| time.sleep(0.01) # ~100 FPS | ||
|
|
||
| print("\nTime limit reached.") | ||
|
|
||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| include "timestamp.fbs"; | ||
|
|
||
| namespace core; | ||
|
|
||
| // Raw state of a 3Dconnexion SpaceMouse-family HID device: the current translation and | ||
| // rotation axis readings and the set of currently-held button indices. Carries no | ||
| // semantic mapping -- which axis/button means what (a position delta, a rotation delta, | ||
| // a toggle) is entirely up to the consuming retargeter. | ||
| // | ||
| // Validated devices: | ||
| // - SpaceMouse Compact, SpaceMouse Wireless, SpaceNavigator for Notebooks, | ||
| // 3Dconnexion Universal Receiver. | ||
| // | ||
| // All fields are always present whenever this table itself is present. | ||
| table SpaceMouseOutput { | ||
| // Translation axis readings [x, y, z], normalized to [-1, 1]. Index i is the | ||
| // value of translation axis i, in the order reported by the device's HID report | ||
| // (see spacemouse_plugin.cpp). | ||
| translation: [float] (id: 0); | ||
|
|
||
| // Rotation axis readings [x, y, z], normalized to [-1, 1], same convention as | ||
| // translation. | ||
| rotation: [float] (id: 1); | ||
|
|
||
| // Button indices currently held down (bit position in the device's button | ||
| // report byte), in no particular order. | ||
| pressed_buttons: [ushort] (id: 2); | ||
|
|
||
| // Whether the tracker has emitted at least one sample. | ||
| is_valid: bool (id: 3); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the record exists then apparently there is at least one sample. The field does not seem to add much value by itself |
||
| } | ||
|
|
||
| // MCAP recording wrapper for SpaceMouseOutput. | ||
| table SpaceMouseOutputRecord { | ||
| data: SpaceMouseOutput (id: 0); | ||
| timestamp: DeviceDataTimestamp (id: 1); | ||
| } | ||
|
|
||
| root_type SpaceMouseOutputRecord; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // Python bindings for the SpaceMouse FlatBuffer schema. | ||
| // Types: SpaceMouseOutput (table), exposed as an encoded view. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "schema_serialized.h" | ||
|
|
||
| #include <pybind11/pybind11.h> | ||
| #include <schema/spacemouse_generated.h> | ||
|
|
||
| #include <cstdint> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| namespace py = pybind11; | ||
|
|
||
| namespace core | ||
| { | ||
|
|
||
| inline void bind_spacemouse(py::module& m) | ||
| { | ||
| serialized_class<SpaceMouseOutput>(m, "SpaceMouseOutput", "Encoded raw SpaceMouse axis/button state.") | ||
| .def(py::init( | ||
| [](std::vector<float> translation, std::vector<float> rotation, std::vector<uint16_t> pressed_buttons, | ||
| bool is_valid) | ||
| { | ||
| SpaceMouseOutputT native; | ||
| native.translation = std::move(translation); | ||
| native.rotation = std::move(rotation); | ||
| native.pressed_buttons = std::move(pressed_buttons); | ||
| native.is_valid = is_valid; | ||
| return pack<SpaceMouseOutput>(native); | ||
| }), | ||
| py::arg("translation"), py::arg("rotation"), py::arg("pressed_buttons"), py::arg("is_valid"), | ||
| "Encode a SpaceMouse axis/button snapshot.") | ||
| .def_property_readonly("translation", vector_field(&SpaceMouseOutput::translation)) | ||
| .def_property_readonly("rotation", vector_field(&SpaceMouseOutput::rotation)) | ||
| .def_property_readonly("pressed_buttons", vector_field(&SpaceMouseOutput::pressed_buttons)) | ||
| .def_property_readonly("is_valid", field(&SpaceMouseOutput::is_valid)) | ||
| .def("__repr__", | ||
| [](const Serialized<SpaceMouseOutput>& self) | ||
| { | ||
| std::string result = "SpaceMouseOutput(pressed_buttons=["; | ||
| const auto* buttons = self->pressed_buttons(); | ||
| if (buttons != nullptr) | ||
| { | ||
| for (size_t i = 0; i < buttons->size(); ++i) | ||
| { | ||
| if (i > 0) | ||
| result += ", "; | ||
| result += std::to_string((*buttons)[i]); | ||
| } | ||
| } | ||
| result += "], is_valid=" + std::to_string(self->is_valid()) + ")"; | ||
| return result; | ||
| }); | ||
|
|
||
| bind_record<SpaceMouseOutputRecord, SpaceMouseOutput>(m, "SpaceMouseOutputRecord", "SpaceMouseOutput"); | ||
| } | ||
|
|
||
| } // namespace core |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") | ||
| message(STATUS "Skipping spacemouse plugin (Linux only)") | ||
| add_custom_target(spacemouse_plugin | ||
| COMMAND ${CMAKE_COMMAND} -E echo "Skipping spacemouse: Linux only") | ||
| return() | ||
| endif() | ||
|
|
||
| add_executable(spacemouse_plugin | ||
| main.cpp | ||
| spacemouse_plugin.cpp | ||
| ) | ||
|
|
||
| target_link_libraries(spacemouse_plugin PRIVATE | ||
| pusherio::pusherio | ||
| oxr::oxr_core | ||
| isaacteleop_schema | ||
| ) | ||
|
|
||
| install(TARGETS spacemouse_plugin RUNTIME DESTINATION plugins/spacemouse) | ||
| install(FILES plugin.yaml README.md DESTINATION plugins/spacemouse) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| <!-- | ||
| SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| SPDX-License-Identifier: Apache-2.0 | ||
| --> | ||
|
|
||
| # SpaceMouse Plugin | ||
|
|
||
| Reads a 3Dconnexion SpaceMouse-family device from `/dev/hidraw*` and pushes `SpaceMouseOutput` | ||
| via OpenXR. Use with `SpaceMouseTracker` with the same `collection_id`. | ||
|
|
||
| Reports raw axis/button state only, with no semantic mapping to position, rotation, or | ||
| commands -- that mapping belongs in a retargeter (e.g. `SpaceMouseToSe3RelRetargeter`) | ||
| consuming this tracker's output. | ||
|
|
||
| Self-discovers its device (the first hidraw device under `/sys/class/hidraw/` whose | ||
| `HID_NAME` matches a validated product name), so it needs no arguments to run and can be | ||
| auto-launched by `PluginManager` via `PluginConfig` -- no manual process to start. | ||
|
|
||
| ## Usage | ||
|
|
||
| Auto-launched (recommended -- matches how `PluginManager` invokes plugins): | ||
|
|
||
| ```bash | ||
| ./spacemouse_plugin --plugin-root-id=spacemouse | ||
| ``` | ||
|
|
||
| Manual / standalone, with an explicit device: | ||
|
|
||
| ```bash | ||
| ./spacemouse_plugin [device_path] [--combined-report] [--plugin-root-id=<collection_id>] | ||
| ``` | ||
|
|
||
| - **device_path**: Optional. Defaults to the first matching hidraw device under | ||
| `/sys/class/hidraw/`. Identify a specific device with | ||
| `cat /sys/class/hidraw/hidraw*/device/uevent` (look for a `HID_NAME=` line) or `lsusb`. | ||
| Reading `/dev/hidraw*` typically requires membership in the `input` group (or a udev | ||
| rule granting access). | ||
| - **--combined-report**: Only needed with an explicit device path for a "3Dconnexion | ||
| Universal Receiver" (auto-discovery sets this automatically); packs translation and | ||
| rotation into a single 13-byte report instead of two separate 7-byte reports. | ||
| - **collection_id**: Default `spacemouse`. Match this when creating `SpaceMouseTracker`. | ||
|
|
||
| ## Validated devices | ||
|
|
||
| - SpaceMouse Compact | ||
| - SpaceMouse Wireless | ||
| - SpaceNavigator for Notebooks | ||
| - 3Dconnexion Universal Receiver | ||
|
|
||
| ## Axis/button mapping | ||
|
|
||
| Reports the current translation (`[x, y, z]`) and rotation (`[x, y, z]`) axis readings, | ||
| normalized to `[-1, 1]`, and the set of currently-held button indices (bit position in the | ||
| device's button report byte). Linux only. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use existing
Poseschema type to represent the transform?