Repository navigation
feat(revohuman): add the RevoHuman glove plugin #1407
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
base: main
Are you sure you want to change the base?
Changes from all commits
0b98b17
da8d0c6
24182bc
3256f31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| .. SPDX-FileCopyrightText: Copyright (c) 2026 BrainCo. All rights reserved. | ||
| .. SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| RevoHuman Glove | ||
| =============== | ||
|
|
||
| A Linux-only plugin for BrainCo RevoHuman exoskeleton gloves. It reads each | ||
| glove's 21 encoder angles and its tactile forces through the RevoHuman C SDK, | ||
| maps the SDK's hand points to the 26 OpenXR hand joints and injects them into | ||
| the OpenXR hand-tracking layer. It also pushes the encoder angles as joint | ||
| states and the forces with the common tactile contract, laid out with the SDK's | ||
| taxel layout. | ||
|
|
||
| .. contents:: On this page | ||
| :local: | ||
| :depth: 2 | ||
|
|
||
| Data flow | ||
| --------- | ||
|
|
||
| .. code-block:: text | ||
|
|
||
| RevoHuman glove (USB) ──► RevoHuman C SDK ─┐ | ||
| ├─► revohuman plugin ──► CloudXR OpenXR runtime | ||
| Optical hand / controller ─────────────────┘ (hand points, (hand joints, joint states, | ||
| wrist-pose fusion) tactile forces) | ||
|
|
||
| The plugin computes no kinematics of its own: the SDK turns the angles into hand | ||
| points for the glove's mechanical model, and the plugin maps those points to | ||
| OpenXR order with corrections reviewed for that model. No URDF is loaded. | ||
|
|
||
| Outputs | ||
| ------- | ||
|
|
||
| Per hand, with the default collection prefix ``revohuman`` | ||
| (``--collection-prefix=NAME`` changes it): | ||
|
|
||
| .. list-table:: | ||
| :widths: 40 60 | ||
| :header-rows: 1 | ||
|
|
||
| * - Output | ||
| - Contents | ||
| * - OpenXR hand ``/hand/left``, ``/hand/right`` | ||
| - 26 joints relative to the wrist, placed at the wrist pose described below | ||
| * - ``revohuman_<side>_joint_state`` / tensor ``joint_state`` | ||
| - ``JointStateOutput`` with the 21 encoder angles in radians, in SDK order; | ||
| see :doc:`/device/joint_space/index` | ||
| * - ``revohuman_<side>_tactile`` / tensor ``tactile`` | ||
| - ``TactileDataArray`` taxel forces: the SDK's calibrated reading, unchanged, | ||
| along each taxel's outward normal; see :doc:`/device/tactile/index` | ||
| * - ``revohuman_<side>_tactile/metadata`` | ||
| - The ``TactileMetadata`` layout the forces belong to | ||
|
|
||
| Started by hand, the plugin publishes ``<id>/device_status`` only with | ||
| ``--plugin-root-id=<id>``. | ||
|
|
||
| Prerequisites | ||
| ------------- | ||
|
|
||
| - **Linux** — x86_64 or aarch64. | ||
| - **Source-build prerequisites** — the toolchain and system packages in | ||
| :doc:`/development/build_from_source/index`, for ``install.sh`` and the manual | ||
| CMake build alike. | ||
| - **RevoHuman C SDK 0.1.3** — the ``c-sdk-v0.1.3`` release of | ||
| `brainco-revohuman-sdk <https://github.com/BrainCoTech/brainco-revohuman-sdk>`_, | ||
| which extracts to ``include/revohuman.h``, ``lib/librevohuman.so`` with its | ||
| versioned names, and ``LICENSE``. The install script downloads it | ||
| automatically. A Python SDK wheel cannot replace it. | ||
| - **RevoHuman gloves** connected over USB, with access to their serial ports. | ||
| - **CloudXR runtime** running on the plugin host. | ||
|
|
||
| Installation | ||
| ------------ | ||
|
|
||
| ``install.sh`` downloads the pinned C SDK release, verifies it against a | ||
| per-architecture SHA-256, extracts it next to the script (re-runs reuse the | ||
| extracted copy), and configures, builds, and installs the plugin with the SDK | ||
| library and its licence next to it: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| ./src/plugins/revohuman/install.sh | ||
|
|
||
| Pass ``--build-dir <path>`` to reuse a non-default top-level CMake build | ||
| directory and ``--install-prefix <dir>`` to choose where the plugin goes. | ||
| ``--sdk-root <dir>`` (or ``REVOHUMAN_SDK_ROOT``) uses an SDK package you already | ||
| extracted. | ||
|
|
||
| To drive CMake directly: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| cmake -B build -DBUILD_PLUGINS=ON -DBUILD_PLUGIN_REVOHUMAN=ON \ | ||
| -DREVOHUMAN_SDK_ROOT=/path/to/revohuman-c-sdk | ||
| cmake --build build --target revohuman_plugin | ||
| cmake --install build --component revohuman | ||
|
|
||
| The installed plugin finds the SDK library next to itself, so it needs neither | ||
| the download nor the build directory afterwards. Keep the library's symlinks. | ||
|
|
||
| Running the Plugin | ||
| ------------------ | ||
|
|
||
| The plugin reaches the Teleop session through the CloudXR / OpenXR runtime, so | ||
| start the runtime first and source its environment in the shell that launches | ||
| the plugin: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| python -m isaaccapture.cloudxr.service start # runs in the background | ||
| source ~/.cloudxr/run/cloudxr.env | ||
| ./install/plugins/revohuman/revohuman_plugin | ||
|
|
||
| See :ref:`dedicated-cloudxr-runtime` and | ||
| :ref:`load-cloudxr-environment-variables` for the full service setup. | ||
| Applications can also launch it through its ``plugin.yaml`` using | ||
| ``TeleopSession``. The gloves are its only source, so it takes no source | ||
| argument; an unknown argument such as ``--live`` or ``--replay`` is ignored with | ||
| a warning. | ||
|
|
||
| The plugin neither records nor plays back: record its collections with | ||
| ``McapRecordingConfig`` and play them back with ``McapReplayConfig``, as for any | ||
| other tracker (see :doc:`/references/mcap_record_replay`). | ||
|
|
||
| The plugin finds the gloves through the SDK, at most one left and one right, | ||
| with nothing to declare. When both are plugged in, it opens them in one | ||
| two-hand SDK kit with time sync on, so the two hands' samples come from the | ||
| same USB frame. A glove plugged in alone opens on its own; when the other one | ||
| arrives, both are reopened together. Gloves that cannot be opened together | ||
| keep streaming on their own. Until a glove is found, the plugin looks for it | ||
| every 2 s and reports the SDK's reason in its status, without holding up the | ||
| other hand. The SDK reconnects a glove after a short dropout; when the stream | ||
| ends anyway, the plugin closes the glove, together with its partner when the | ||
| two share a kit, and looks for them again. | ||
|
|
||
| Each glove's mechanical model follows the product the SDK identifies: a PTV1 | ||
| glove uses the DV1 model and a P1 glove the P model. A P1 glove has no tactile | ||
| sensors, so it streams the hand and joint states without tactile outputs. A | ||
| glove of the wrong product or with incompatible firmware is reported as failed. | ||
|
|
||
| Wrist pose | ||
| ---------- | ||
|
|
||
| The glove stream contains wrist-relative joints but no absolute 6DoF wrist | ||
| pose. Before injection, the plugin places the hand at a wrist pose from optical | ||
| hand tracking or from a controller aim pose combined with a rigid per-hand | ||
| offset. | ||
|
|
||
| Set ``REVOHUMAN_WRIST_SOURCE`` (or pass ``--wrist-source``) to ``auto`` (the | ||
| default), ``hand_tracking``, or ``controller``. In ``auto`` mode, optical | ||
| tracking is preferred; the controller is used when optical tracking provides no | ||
| valid pose. The plugin refuses a ``--wrist-source`` that disagrees with the | ||
| variable. | ||
|
|
||
| A controller reports its aim pose, not the wrist. No controller mount is | ||
| calibrated for these gloves yet, so the aim-to-wrist offsets default to | ||
| identity and the plugin warns for each hand without one unless the source is | ||
| ``hand_tracking``. Set them with: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| ./install/plugins/revohuman/revohuman_plugin \ | ||
| --aim-to-wrist-left=px,py,pz,qx,qy,qz,qw \ | ||
| --aim-to-wrist-right=px,py,pz,qx,qy,qz,qw | ||
|
|
||
| Position values are in meters and quaternion values use ``(x, y, z, w)``. | ||
|
|
||
| Without a tracked wrist the hand is still injected, without tracking flags, at | ||
| the last valid wrist pose (or the stage origin), and its status is degraded. | ||
|
|
||
| The glove's wrist is a reference point of its mechanical model: the origin of | ||
| its ``palm_link``, on the back of the hand near the end of the forearm, moved | ||
| about 12.6 mm towards the palm to the depth of the palm centre. It is not the | ||
| anatomical wrist centre that the OpenXR wrist joint stands for. Nominally the | ||
| wrist centre is about 4 cm further towards the fingers and about 0.2 to 0.7 cm | ||
| further towards the palm; these figures come from typical adult hand | ||
| dimensions, not from measurements on the glove, and vary with the wearer. With | ||
| optical hand tracking, the injected hand therefore sits that much further | ||
| towards the fingers and the palm than the tracked hand. With a controller, set | ||
| the aim-to-wrist offsets to end at the glove's wrist point. Verify the | ||
| alignment for the wearer. | ||
|
|
||
| Tactile layout | ||
| -------------- | ||
|
|
||
| At start-up the plugin builds each hand's ``TactileMetadata`` from the C SDK's | ||
| taxel layout for the DV1 model (``rh_hand_model_taxel_layout``); there is no | ||
| layout file or option. Each taxel is bound rigidly to the OpenXR joint its SDK | ||
| link moves with, with its +Z along the outward normal. That joint is rigid on | ||
| the link, so the skinned taxels stay on the SDK's taxels as the fingers bend. | ||
| The log names the layout source, taxel count and SHA-256 of each hand's | ||
| metadata. When the SDK has no layout, the plugin logs why and pushes no tactile | ||
| data; the other outputs are unaffected. | ||
|
|
||
| A force sample is pushed only while it is calibrated, recent and newer than the | ||
| last one, and its status names the reason when it is not. Consumers must check | ||
| that status as well as the presence of data. | ||
|
|
||
| Troubleshooting | ||
| --------------- | ||
|
|
||
| .. list-table:: | ||
| :widths: 40 60 | ||
| :header-rows: 1 | ||
|
|
||
| * - Symptom | ||
| - Fix | ||
| * - ``install.sh`` fails with a SHA-256 mismatch | ||
| - The downloaded package does not match the pinned release. Re-run to | ||
| rule out a corrupted download; if it persists, do not bypass the check | ||
| — verify the release assets upstream. | ||
| * - No ``revohuman_plugin`` is built, ``install.sh`` refuses the install, or | ||
| the plugin stops before the OpenXR session naming an SDK function | ||
| - The SDK found during configuration is incomplete: its library is missing | ||
| or lacks a function the plugin calls. Point ``REVOHUMAN_SDK_ROOT`` at a | ||
| complete C SDK release and reconfigure. | ||
| * - A glove is never opened and its status names a busy port | ||
| - Another process, such as the SDK viewer, holds the glove's serial port. | ||
| Close it; the plugin opens the glove on its next attempt. | ||
| * - A glove is reported as failed | ||
| - The status carries the SDK's reason: a product or variant the plugin | ||
| does not support, or incompatible firmware. These are not retried. | ||
| * - Hands appear but a downstream client sees no tactile data | ||
| - A P1 glove has no tactile sensors. Otherwise check the start-up log for | ||
| why there is no ``TactileMetadata`` (an SDK without the taxel layout), | ||
| and the tactile status for the reason a sample was not pushed. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 BrainCo. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| # Vendor SDK extracted by install.sh | ||
| revohuman-c-sdk-*/ |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,107 @@ | ||||||
| # SPDX-FileCopyrightText: Copyright (c) 2026 BrainCo. All rights reserved. | ||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||
|
|
||||||
| # RevoHuman exoskeleton gloves: OpenXR hands, encoder joint states and tactile data with its | ||||||
| # TactileMetadata, pushed via OpenXR. | ||||||
|
|
||||||
| # Outcome of this configure for scripts such as install.sh, which must not accept a build without | ||||||
| # the plugin; OFF until the live source is known to link, so an early return below leaves it OFF. | ||||||
| set(REVOHUMAN_LIVE_AVAILABLE OFF CACHE INTERNAL "RevoHuman live source links against the C SDK") | ||||||
|
|
||||||
| if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") | ||||||
| message(STATUS "Skipping revohuman plugin (Linux only)") | ||||||
| add_custom_target(revohuman_plugin | ||||||
| COMMAND ${CMAKE_COMMAND} -E echo "Skipping revohuman: Linux only") | ||||||
| return() | ||||||
| endif() | ||||||
|
|
||||||
| # The plugin needs the C SDK header (revohuman.h); the live source also needs its library. | ||||||
| # There is no default location: without the cache variable or the environment variable the | ||||||
| # plugin is skipped, as it is when the header is missing. install.sh always passes it. | ||||||
| set(REVOHUMAN_SDK_ROOT "" CACHE PATH "RevoHuman C SDK root (contains include/revohuman.h and lib/)") | ||||||
| if(NOT REVOHUMAN_SDK_ROOT AND DEFINED ENV{REVOHUMAN_SDK_ROOT}) | ||||||
| set(REVOHUMAN_SDK_ROOT "$ENV{REVOHUMAN_SDK_ROOT}") | ||||||
| endif() | ||||||
| set(_revohuman_no_sdk "") | ||||||
| if(NOT REVOHUMAN_SDK_ROOT) | ||||||
| set(_revohuman_no_sdk "RevoHuman C SDK root not set (set -DREVOHUMAN_SDK_ROOT=/path/to/sdk or REVOHUMAN_SDK_ROOT)") | ||||||
| elseif(NOT EXISTS "${REVOHUMAN_SDK_ROOT}/include/revohuman.h") | ||||||
| set(_revohuman_no_sdk "RevoHuman C SDK not found (set -DREVOHUMAN_SDK_ROOT=/path/to/sdk)") | ||||||
| endif() | ||||||
| if(_revohuman_no_sdk) | ||||||
| message(STATUS "${_revohuman_no_sdk} — skipping revohuman plugin build.") | ||||||
| return() | ||||||
| endif() | ||||||
| set(REVOHUMAN_SDK_INCLUDE_DIR "${REVOHUMAN_SDK_ROOT}/include") | ||||||
|
|
||||||
| add_library(revohuman_sdk_headers INTERFACE) | ||||||
| target_include_directories(revohuman_sdk_headers INTERFACE $<BUILD_INTERFACE:${REVOHUMAN_SDK_INCLUDE_DIR}>) | ||||||
| add_library(revohuman_plugin::sdk_headers ALIAS revohuman_sdk_headers) | ||||||
|
|
||||||
| # The library is used only when it links with every function the live source calls, so a partial | ||||||
| # SDK build skips the plugin executable instead of failing the build. Checked on every configure, | ||||||
| # and searched again whenever the SDK root changes (find_library alone keeps its first result). | ||||||
| if(NOT "${REVOHUMAN_SDK_ROOT}" STREQUAL "${_REVOHUMAN_SDK_LIBRARY_ROOT}") | ||||||
| unset(REVOHUMAN_SDK_LIBRARY CACHE) | ||||||
| set(_REVOHUMAN_SDK_LIBRARY_ROOT "${REVOHUMAN_SDK_ROOT}" CACHE INTERNAL "SDK root REVOHUMAN_SDK_LIBRARY was found in") | ||||||
| endif() | ||||||
| find_library(REVOHUMAN_SDK_LIBRARY NAMES revohuman PATHS "${REVOHUMAN_SDK_ROOT}/lib" NO_DEFAULT_PATH) | ||||||
| set(REVOHUMAN_LIVE_AVAILABLE OFF) | ||||||
| if(REVOHUMAN_SDK_LIBRARY) | ||||||
| include(CheckCXXSourceCompiles) | ||||||
| include(CMakePushCheckState) | ||||||
| cmake_push_check_state(RESET) | ||||||
| set(CMAKE_REQUIRED_INCLUDES "${REVOHUMAN_SDK_INCLUDE_DIR}") | ||||||
| set(CMAKE_REQUIRED_LIBRARIES "${REVOHUMAN_SDK_LIBRARY}") | ||||||
| set(CMAKE_REQUIRED_QUIET ON) | ||||||
| unset(REVOHUMAN_SDK_LINKS CACHE) | ||||||
| check_cxx_source_compiles([=[ | ||||||
|
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. this list mirrors the calls in live_source.cpp by hand and will drift; linking the plugin checks the same thing and fails loudly instead of silently dropping the executable |
||||||
| #include <revohuman.h> | ||||||
| using Fn = void (*)(); | ||||||
| #define RH_FN(f) reinterpret_cast<Fn>(&f) | ||||||
| int main(int argc, char**) | ||||||
| { | ||||||
| const Fn used[] = { | ||||||
| RH_FN(rh_init_options_init), RH_FN(rh_init), RH_FN(rh_shutdown), RH_FN(rh_version), | ||||||
| RH_FN(rh_set_log_callback), RH_FN(rh_last_error), RH_FN(rh_last_error_code), RH_FN(rh_error_class_name), | ||||||
| RH_FN(rh_kit_options_init), RH_FN(rh_kit_open), | ||||||
| RH_FN(rh_kit_close), RH_FN(rh_kit_identity), RH_FN(rh_kit_glove), RH_FN(rh_glove_info), | ||||||
| RH_FN(rh_joint_name), RH_FN(rh_joints_to_radians), RH_FN(rh_subscribe_options_init), | ||||||
| RH_FN(rh_glove_subscribe), RH_FN(rh_sub_recv_glove), RH_FN(rh_sub_dropped), RH_FN(rh_sub_close), | ||||||
| RH_FN(rh_glove_capture_ok), RH_FN(rh_glove_capture_cause), RH_FN(rh_scan_options_init), | ||||||
| RH_FN(rh_scan), RH_FN(rh_scan_count), RH_FN(rh_scan_get), RH_FN(rh_scan_free), | ||||||
| }; | ||||||
| return used[argc] == nullptr; | ||||||
| } | ||||||
| ]=] REVOHUMAN_SDK_LINKS) | ||||||
| cmake_pop_check_state() | ||||||
| if(REVOHUMAN_SDK_LINKS) | ||||||
| set(REVOHUMAN_LIVE_AVAILABLE ON) | ||||||
| add_library(revohuman_plugin::sdk UNKNOWN IMPORTED) | ||||||
|
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.
Suggested change
so tests/cpp/plugins/revohuman/integration can link the target instead of the raw library path (its CMakeLists.txt:23) |
||||||
| set_target_properties(revohuman_plugin::sdk PROPERTIES | ||||||
| IMPORTED_LOCATION "${REVOHUMAN_SDK_LIBRARY}" | ||||||
| INTERFACE_INCLUDE_DIRECTORIES "${REVOHUMAN_SDK_INCLUDE_DIR}") | ||||||
| message(STATUS "RevoHuman C SDK library ${REVOHUMAN_SDK_LIBRARY}: live source available " | ||||||
| "(installed next to the plugin)") | ||||||
| else() | ||||||
| message(STATUS "RevoHuman C SDK library ${REVOHUMAN_SDK_LIBRARY} lacks functions the live source " | ||||||
|
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. the user asked for the plugin and the SDK is there but incomplete; fail the configure like sharpa_avatar does for a failed SDK check rather than quietly skipping the executable |
||||||
| "calls — skipping the plugin executable") | ||||||
| endif() | ||||||
| else() | ||||||
| message(STATUS "RevoHuman C SDK library not found in ${REVOHUMAN_SDK_ROOT}/lib — skipping the plugin executable") | ||||||
| endif() | ||||||
| set(REVOHUMAN_LIVE_AVAILABLE "${REVOHUMAN_LIVE_AVAILABLE}" | ||||||
| CACHE INTERNAL "RevoHuman live source links against the C SDK") | ||||||
|
|
||||||
| add_subdirectory(core) | ||||||
| add_subdirectory(live) | ||||||
| if(REVOHUMAN_LIVE_AVAILABLE) | ||||||
| add_subdirectory(kinematics) | ||||||
| endif() | ||||||
| add_subdirectory(app) | ||||||
|
|
||||||
| # One component for the plugin, its manifest, README and the SDK library (app/), so install.sh installs | ||||||
| # exactly these; a plain `cmake --install` still installs every component. No plugin, no manifest. | ||||||
| if(REVOHUMAN_LIVE_AVAILABLE) | ||||||
| install(FILES plugin.yaml README.md DESTINATION plugins/revohuman COMPONENT revohuman) | ||||||
| endif() | ||||||
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.
REVOHUMAN_LIVE_AVAILABLEis written four times in this file (9, 49, 79, 93) and this normal var shadows the cache one; keep the reset at 9, write 79 asCACHE INTERNAL(INTERNAL implies FORCE) and drop this one and 93