Skip to content

feat: add Haply Robotics hand tracking plugin#267

Open
vickybot911 wants to merge 3 commits intoNVIDIA:mainfrom
vickybot911:feature/haply-plugin-impl
Open

feat: add Haply Robotics hand tracking plugin#267
vickybot911 wants to merge 3 commits intoNVIDIA:mainfrom
vickybot911:feature/haply-plugin-impl

Conversation

@vickybot911
Copy link

@vickybot911 vickybot911 commented Mar 14, 2026

Summary

Add a new plugin for Haply Robotics Inverse3 and VerseGrip devices at src/plugins/haply/, following the same architecture as the existing Manus plugin.

Components

  • haply_plugin_core (libIsaacTeleopPluginsHaply.so) — Core library with WebSocket client, JSON parsing, OpenXR HandInjector integration, and DeviceIO controller tracking
  • haply_hand_plugin — Plugin executable running at 90Hz
  • haply_hand_tracker_printer — Standalone CLI tool for verifying Haply device data (no OpenXR dependency)

Architecture

  • Background I/O thread communicates with Haply SDK via WebSocket (ws://localhost:10001)
  • Maps Inverse3 cursor position + VerseGrip orientation → OpenXR XrHandJointLocationEXT[26]
  • WRIST and PALM get real tracked data; finger joints placed at wrist with VALID but not TRACKED flags
  • Controller aim pose provides root positioning (same pattern as Manus plugin)
  • Exponential backoff reconnection on WebSocket errors

Mapping

Joint Source locationFlags
WRIST Inverse3 cursor + VerseGrip orientation VALID + TRACKED
PALM Same as WRIST VALID + TRACKED
Finger joints (23) Wrist position, identity orientation VALID only

Configuration

Env Variable Default Description
HAPLY_WEBSOCKET_HOST 127.0.0.1 Haply SDK WebSocket host
HAPLY_WEBSOCKET_PORT 10001 Haply SDK WebSocket port

Dependencies

  • Build time: No external SDK required
  • Runtime: Haply SDK / Inverse Service
  • Vendored: nlohmann/json (MIT)

Checklist

  • Matches Manus plugin architecture (OpenXR + HandInjector + DeviceIO)
  • Same code style and SPDX headers
  • Thread-safe state management
  • Reconnection with exponential backoff
  • plugin.yaml manifest
  • README documentation
  • CMake presets
  • clang-format clean

Tests in separate PR.

Summary by CodeRabbit

  • New Features

    • Added Haply hand‑tracking plugin with Inverse3 cursor and VerseGrip orientation; OpenXR hand-joint injection at runtime
    • Added standalone executable to run the Haply plugin and a CLI tool to print Haply device state
  • Documentation

    • Added README and plugin manifest with build, run, and troubleshooting instructions
  • Configuration

    • Build presets added; WebSocket host/port configurable via environment variables

Add a new plugin for Haply Robotics Inverse3 and VerseGrip devices,
bridging the Haply SDK WebSocket interface to the Isaac Teleop framework
via OpenXR hand tracking injection.

Components:
- haply_plugin_core: shared library (libIsaacTeleopPluginsHaply.so)
  implementing WebSocket client, JSON parsing, OpenXR HandInjector
  integration, and DeviceIO controller tracking
- haply_hand_plugin: plugin executable running at 90Hz
- haply_hand_tracker_printer: standalone CLI tool for verifying Haply
  device data without OpenXR dependencies

Architecture:
- Background I/O thread communicates with Haply SDK via WebSocket
  (ws://localhost:10001, configurable via HAPLY_WEBSOCKET_HOST/PORT)
- Maps Inverse3 cursor position and VerseGrip orientation to OpenXR
  XrHandJointLocationEXT format (26 joints)
- WRIST and PALM joints receive real tracked data; finger joints are
  placed at wrist position with VALID but not TRACKED flags
- Controller aim pose provides root positioning (same as Manus plugin)
- Exponential backoff reconnection on WebSocket errors

No build-time SDK dependency — Haply SDK is a runtime service.
Vendored: nlohmann/json (MIT, single header) for JSON parsing.

Signed-off-by: Vicky <vickybot911@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 14, 2026

📝 Walkthrough

Walkthrough

Adds a new Haply hand‑tracking plugin: CMake build integration, core shared library with a WebSocket client and OpenXR injection, a runtime executable (90 Hz update loop), a CLI printer tool, plugin manifest, and documentation.

Changes

Cohort / File(s) Summary
Top-level build
CMakeLists.txt
Includes haply plugin under BUILD_PLUGINS.
Plugin root & metadata
src/plugins/haply/CMakeLists.txt, src/plugins/haply/CMakePresets.json, src/plugins/haply/CMakePresets.json.license, src/plugins/haply/.gitignore, src/plugins/haply/README.md
Adds plugin CMake, presets, license file, .gitignore, and README. Review presets and licensing text.
Core library & headers
src/plugins/haply/core/CMakeLists.txt, src/plugins/haply/inc/core/haply_hand_tracking_plugin.hpp, src/plugins/haply/core/haply_hand_tracking_plugin.cpp
Adds shared library target haply_plugin_core, public include, new types, HaplyWebSocket and HaplyTracker (I/O thread, JSON parsing, OpenXR injection). Pay attention to concurrency, socket/frame parsing, and OpenXR integration.
Runtime executable
src/plugins/haply/app/CMakeLists.txt, src/plugins/haply/app/main.cpp
Adds haply_hand_plugin executable implementing singleton use and a ~90 Hz update loop with signal handling and error reporting.
CLI tool
src/plugins/haply/tools/CMakeLists.txt, src/plugins/haply/tools/haply_hand_tracker_printer.cpp
Adds haply_hand_tracker_printer with embedded MiniWebSocket, reconnect/backoff, JSON parsing, env var config, and periodic console output. Validate reconnection/backoff and parsing logic.
Plugin manifest
src/plugins/haply/plugin.yaml
Adds plugin manifest describing haply_hand_plugin, command, version, and a /hand/right hand_tracker device entry.

Sequence Diagram(s)

sequenceDiagram
    participant App as haply_hand_plugin
    participant Tracker as HaplyTracker
    participant WS as HaplyWebSocket
    participant SDK as Haply SDK
    participant Injector as HandInjector
    participant OXR as OpenXR

    App->>Tracker: instance() / update()
    activate Tracker
    Tracker->>WS: connect(host,port,path)
    WS->>SDK: WebSocket HTTP upgrade (101)
    SDK-->>WS: 101 Switching Protocols

    loop ~11.11ms (90 Hz)
        App->>Tracker: update()
        Tracker->>WS: recv_text(JSON)
        Tracker->>Tracker: parse JSON -> update internal state
        Tracker->>Injector: inject_hand_data()
        Injector->>OXR: submit hand joint poses
        OXR-->>Injector: ack
    end

    App->>Tracker: shutdown()
    Tracker->>WS: close()
    deactivate Tracker
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A tiny thump, a socket hum,
I hop the frames and parse the drum,
Ninety beats the fingers trace,
From Haply stream to OpenXR space,
A rabbit cheers: plugin's alive!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Haply Robotics hand tracking plugin' directly and clearly summarizes the main change: adding a new Haply hand-tracking plugin component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/haply/app/main.cpp`:
- Around line 24-33: The infinite loop using while (true) that repeatedly calls
tracker.update() and sleeps_until(frame_start + target_frame_duration) makes the
trailing return 0 unreachable; either remove the unreachable return statement or
convert the loop to use a mutable running flag checked in the condition (e.g.,
bool running = true; while (running) { ... }) and install a signal handler to
set running = false for graceful shutdown so that tracker.update() can exit
cleanly and the function can return normally.

In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp`:
- Around line 409-412: The port parsing using std::atoi is unsafe because it
returns 0 on failure; in haply_hand_tracking_plugin.cpp replace the
std::atoi-based parsing for port_env/ws_port with std::stoul-based parsing:
first check port_env is non-null and non-empty, call std::stoul inside a
try/catch, validate the resulting unsigned long is <= 65535 and > 0, then
static_cast to uint16_t; on exception or out-of-range value fall back to the
default 10001 and optionally log the invalid value so ws_port always gets a
validated port number.
- Line 320: Validate and bound payload_len before allocating
std::vector<uint8_t> data(payload_len): define a sensible MAX_PAYLOAD_SIZE
(e.g., a few MB), check if payload_len == 0 or payload_len > MAX_PAYLOAD_SIZE,
and reject the frame by logging an error and returning/closing the WebSocket
instead of allocating; update the allocation site that uses payload_len and data
to use the validated length and ensure any error path cleans up/avoids
processing the malformed frame.
- Around line 710-715: The field m_grip_interpolant is computed from
snapshot.buttons (button_0..3) with grip_speed but never used to influence
finger poses, so either (A) add a clear TODO comment above this block explaining
the intended use (e.g., "TODO: apply m_grip_interpolant to finger joint
positions / blend wrist->fist pose") so future work knows this smoothing is
intentional, or (B) remove the computation here and delete the
m_grip_interpolant declaration from the class header to avoid dead state;
reference m_grip_interpolant, snapshot.buttons, and grip_speed when making the
change so the correct code is updated.

In `@src/plugins/haply/README.md`:
- Line 74: Replace the British spelling "Synthesised" with the American spelling
"Synthesized" in the README entry for "**Finger joints**" so the line reads
"**Finger joints**: Synthesized at the wrist position (valid but not
individually tracked)"; locate the phrase "**Finger joints**" in
src/plugins/haply/README.md and update that word only to preserve the rest of
the sentence.

In `@src/plugins/haply/tools/haply_hand_tracker_printer.cpp`:
- Around line 31-33: The lightweight WebSocket client duplicated in
haply_hand_tracker_printer.cpp should be factored into a reusable header-only
helper to avoid future duplication; create a new header (e.g.,
websocket_helper.h) that exposes clear functions like connectWebSocket,
sendWebSocketMessage, receiveWebSocketMessage, and closeWebSocket, move the
WebSocket implementation out of haply_hand_tracker_printer.cpp into that header,
update haply_hand_tracker_printer.cpp to call these helper functions, and ensure
the helper is self-contained with only standard or project-wide dependencies so
other tools can include it without pulling plugin internals.
- Around line 307-310: The port parsing using std::atoi for port_env is unsafe;
update the logic (around host_env/port_env, host, and port variables in
haply_hand_tracker_printer.cpp) to parse the environment string with std::stoul
(or equivalent), catch std::invalid_argument/std::out_of_range, validate the
resulting value is within 1–65535, and only then static_cast to uint16_t; on any
parse/validation failure fall back to the default port 10001 and/or log the
invalid value so connections don't silently use 0.
- Around line 184-188: The code allocates std::vector<uint8_t> data(plen) using
an unvalidated plen parsed from the wire; add a sanity check before allocating:
ensure plen is non-negative and <= a defined upper limit (e.g., kMaxPayloadSize
or MAX_PAYLOAD_BYTES) and return false (or handle error) if it exceeds the
limit, then proceed to allocate and call recv_raw; reference and update the
check at the place where plen is used and where recv_raw(...) is called so
malformed frames cannot cause OOM.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cc3305fd-bb37-40af-a6f8-fe9a1b93e7cc

📥 Commits

Reviewing files that changed from the base of the PR and between dd28233 and dbe0aa1.

📒 Files selected for processing (15)
  • CMakeLists.txt
  • src/plugins/haply/.gitignore
  • src/plugins/haply/CMakeLists.txt
  • src/plugins/haply/CMakePresets.json
  • src/plugins/haply/CMakePresets.json.license
  • src/plugins/haply/README.md
  • src/plugins/haply/app/CMakeLists.txt
  • src/plugins/haply/app/main.cpp
  • src/plugins/haply/core/CMakeLists.txt
  • src/plugins/haply/core/haply_hand_tracking_plugin.cpp
  • src/plugins/haply/inc/core/haply_hand_tracking_plugin.hpp
  • src/plugins/haply/plugin.yaml
  • src/plugins/haply/third_party/nlohmann/json.hpp
  • src/plugins/haply/tools/CMakeLists.txt
  • src/plugins/haply/tools/haply_hand_tracker_printer.cpp

Comment on lines +31 to +33
// ============================================================================
// Lightweight WebSocket client (duplicated from the plugin to stay standalone)
// ============================================================================
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Code duplication is acknowledged; consider future consolidation.

The comment notes this WebSocket implementation is duplicated to keep the tool standalone. This is acceptable for a CLI diagnostic utility, but if more tools emerge, extracting a shared header-only WebSocket helper could reduce maintenance burden.

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

In `@src/plugins/haply/tools/haply_hand_tracker_printer.cpp` around lines 31 - 33,
The lightweight WebSocket client duplicated in haply_hand_tracker_printer.cpp
should be factored into a reusable header-only helper to avoid future
duplication; create a new header (e.g., websocket_helper.h) that exposes clear
functions like connectWebSocket, sendWebSocketMessage, receiveWebSocketMessage,
and closeWebSocket, move the WebSocket implementation out of
haply_hand_tracker_printer.cpp into that header, update
haply_hand_tracker_printer.cpp to call these helper functions, and ensure the
helper is self-contained with only standard or project-wide dependencies so
other tools can include it without pulling plugin internals.

- Add WebSocket payload size limit (16 MB) to prevent OOM on malformed frames
- Add SO_RCVTIMEO (5s) connect timeout to prevent indefinite blocking
- Replace std::atoi with std::stoul + range validation for port parsing
- Add signal handler for graceful shutdown in plugin executable
- Add TODO comment for unused m_grip_interpolant (future finger synthesis)
- Fix British spelling: Synthesised → Synthesized
- Apply same payload limit and port validation fixes to printer tool

Signed-off-by: Vicky <vickybot911@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp`:
- Around line 658-666: The lambda process_hand currently calls injector.reset()
unconditionally when the hand doesn't match; change it to only reset when
injector is non-null to avoid redundant resets—i.e., check the unique_ptr
(injector) for truthiness before calling reset (affects calls like
process_hand(false, m_right_injector) / process_hand(true, m_left_injector) and
the injector parameter inside the lambda).
- Around line 420-423: The code reads environment variables using getenv with
keys "HAPLY_WS_HOST" and "HAPLY_WS_PORT"; update these getenv calls to use the
documented names "HAPLY_WEBSOCKET_HOST" and "HAPLY_WEBSOCKET_PORT" so they match
README and the printer tool, keeping the existing fallbacks (ws_host default
"127.0.0.1" and ws_port default 10001) and the same variables (host_env,
port_env, ws_host, ws_port) to minimize other changes.

In `@src/plugins/haply/tools/haply_hand_tracker_printer.cpp`:
- Around line 79-82: The MiniWebSocket code in haply_hand_tracker_printer.cpp
currently sets TCP_NODELAY on fd_ but lacks the receive timeout used by
HaplyWebSocket::connect(); add a SO_RCVTIMEO setsockopt call after the existing
setsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, ...) to set a 5-second timeout (create
a struct timeval tv with tv_sec=5, tv_usec=0 and pass sizeof(tv) with
SOL_SOCKET, SO_RCVTIMEO). Ensure you reference fd_ and reuse the same error
handling pattern around setsockopt as other socket options.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: de57bafd-2e55-4c91-a694-f926c275f8dc

📥 Commits

Reviewing files that changed from the base of the PR and between dbe0aa1 and b00417e.

📒 Files selected for processing (4)
  • src/plugins/haply/README.md
  • src/plugins/haply/app/main.cpp
  • src/plugins/haply/core/haply_hand_tracking_plugin.cpp
  • src/plugins/haply/tools/haply_hand_tracker_printer.cpp

Comment on lines +658 to +666
auto process_hand = [&](bool target_is_left, std::unique_ptr<plugin_utils::HandInjector>& injector)
{
// Only process the hand that matches the Haply device handedness
if (target_is_left != is_left)
{
// Other hand has no data — reset injector so runtime sees isActive=false
injector.reset();
return;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider caching handedness to avoid redundant injector resets.

Currently, if the device is configured for the left hand, m_right_injector.reset() is called every frame in process_hand(false, m_right_injector). While harmless (resetting a null unique_ptr is a no-op after the first call), the logic could be slightly cleaner by checking if the injector is already null before resetting.

♻️ Optional: Skip redundant reset
         // Only process the hand that matches the Haply device handedness
         if (target_is_left != is_left)
         {
             // Other hand has no data — reset injector so runtime sees isActive=false
-            injector.reset();
+            if (injector)
+            {
+                injector.reset();
+            }
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp` around lines 658 -
666, The lambda process_hand currently calls injector.reset() unconditionally
when the hand doesn't match; change it to only reset when injector is non-null
to avoid redundant resets—i.e., check the unique_ptr (injector) for truthiness
before calling reset (affects calls like process_hand(false, m_right_injector) /
process_hand(true, m_left_injector) and the injector parameter inside the
lambda).

- Rename HAPLY_WS_HOST/HAPLY_WS_PORT to HAPLY_WEBSOCKET_HOST/HAPLY_WEBSOCKET_PORT
  in core plugin to match README and printer tool
- Add SO_RCVTIMEO (5s) to printer tool's MiniWebSocket for consistency

Signed-off-by: Vicky <vickybot911@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/plugins/haply/core/haply_hand_tracking_plugin.cpp (1)

660-668: 🧹 Nitpick | 🔵 Trivial

Consider guarding redundant injector.reset() calls.

When the hand doesn't match the device handedness, injector.reset() is called every frame. After the first call, resetting a null unique_ptr is a no-op. While harmless, a guard would clarify intent.

♻️ Optional: Skip redundant reset
         // Only process the hand that matches the Haply device handedness
         if (target_is_left != is_left)
         {
             // Other hand has no data — reset injector so runtime sees isActive=false
-            injector.reset();
+            if (injector)
+            {
+                injector.reset();
+            }
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp` around lines 660 -
668, The lambda process_hand currently calls injector.reset() unconditionally
when the hand doesn't match is_left; change this to only call reset if the
unique_ptr is non-null to avoid redundant no-op resets and clarify intent:
replace the unconditional injector.reset() with a guarded call (e.g., if
(injector) injector.reset()) inside the process_hand lambda that takes
std::unique_ptr<plugin_utils::HandInjector>& injector and checks target_is_left
against is_left.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp`:
- Around line 379-388: HaplyTracker::update currently dereferences
m_deviceio_session without a null check; add a guard at the start of
HaplyTracker::update to return early if m_deviceio_session is null (e.g., if
(!m_deviceio_session) return;), so callers who catch initialize() exceptions
won't hit a null pointer when calling update(), then proceed to call
m_deviceio_session->update() and inject_hand_data() only when the session
exists.
- Around line 544-634: The code holds m_state_mutex while calling ws.send_text
which can block; to fix, capture the needed value (m_state.inverse3_device_id)
into a local string while holding the lock (lock_guard on m_state_mutex), then
release the lock and construct/send the zero-force command with ws.send_text
using that local variable; update the block around m_state.inverse3_device_id
and ws.send_text (referencing m_state_mutex, m_state.inverse3_device_id, and
ws.send_text) so no mutex is held during the send.

---

Duplicate comments:
In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp`:
- Around line 660-668: The lambda process_hand currently calls injector.reset()
unconditionally when the hand doesn't match is_left; change this to only call
reset if the unique_ptr is non-null to avoid redundant no-op resets and clarify
intent: replace the unconditional injector.reset() with a guarded call (e.g., if
(injector) injector.reset()) inside the process_hand lambda that takes
std::unique_ptr<plugin_utils::HandInjector>& injector and checks target_is_left
against is_left.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a512ea8-c614-41ee-aa01-f9c6fb84d9ab

📥 Commits

Reviewing files that changed from the base of the PR and between b00417e and 645f7ac.

📒 Files selected for processing (2)
  • src/plugins/haply/core/haply_hand_tracking_plugin.cpp
  • src/plugins/haply/tools/haply_hand_tracker_printer.cpp

Comment on lines +379 to +388
void HaplyTracker::update()
{
// Update DeviceIOSession which handles time conversion and tracker updates internally
if (!m_deviceio_session->update())
{
return;
}

inject_hand_data();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing null check on m_deviceio_session before use.

If initialize() throws an exception (lines 489-497), the HaplyTracker object is constructed but m_deviceio_session remains null. Callers that catch the exception and still call update() would dereference a null pointer.

🛡️ Proposed fix
 void HaplyTracker::update()
 {
+    if (!m_deviceio_session)
+    {
+        return;
+    }
+
     // Update DeviceIOSession which handles time conversion and tracker updates internally
     if (!m_deviceio_session->update())
     {
         return;
     }
 
     inject_hand_data();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp` around lines 379 -
388, HaplyTracker::update currently dereferences m_deviceio_session without a
null check; add a guard at the start of HaplyTracker::update to return early if
m_deviceio_session is null (e.g., if (!m_deviceio_session) return;), so callers
who catch initialize() exceptions won't hit a null pointer when calling
update(), then proceed to call m_deviceio_session->update() and
inject_hand_data() only when the session exists.

Comment on lines +544 to +634
try
{
json j = json::parse(msg);

std::lock_guard<std::mutex> lock(m_state_mutex);

// Parse inverse3 array
if (j.contains("inverse3") && j["inverse3"].is_array() && !j["inverse3"].empty())
{
const auto& inv3 = j["inverse3"][0];
if (inv3.contains("device_id"))
{
m_state.inverse3_device_id = inv3["device_id"].get<std::string>();
}
// Handedness from config (only in first message)
if (inv3.contains("config") && inv3["config"].contains("handedness"))
{
m_state.handedness = inv3["config"]["handedness"].get<std::string>();
}
if (inv3.contains("state"))
{
const auto& st = inv3["state"];
if (st.contains("cursor_position"))
{
const auto& pos = st["cursor_position"];
m_state.cursor_position.x = pos.value("x", 0.0f);
m_state.cursor_position.y = pos.value("y", 0.0f);
m_state.cursor_position.z = pos.value("z", 0.0f);
}
if (st.contains("cursor_velocity"))
{
const auto& vel = st["cursor_velocity"];
m_state.cursor_velocity.x = vel.value("x", 0.0f);
m_state.cursor_velocity.y = vel.value("y", 0.0f);
m_state.cursor_velocity.z = vel.value("z", 0.0f);
}
}
m_state.has_data = true;
}

// Parse wireless_verse_grip array
if (j.contains("wireless_verse_grip") && j["wireless_verse_grip"].is_array() &&
!j["wireless_verse_grip"].empty())
{
const auto& vg = j["wireless_verse_grip"][0];
if (vg.contains("device_id"))
{
m_state.versegrip_device_id = vg["device_id"].get<std::string>();
}
if (vg.contains("state"))
{
const auto& st = vg["state"];
if (st.contains("orientation"))
{
const auto& ori = st["orientation"];
m_state.orientation.w = ori.value("w", 1.0f);
m_state.orientation.x = ori.value("x", 0.0f);
m_state.orientation.y = ori.value("y", 0.0f);
m_state.orientation.z = ori.value("z", 0.0f);
}
if (st.contains("buttons"))
{
const auto& btn = st["buttons"];
m_state.buttons.button_0 = btn.value("button_0", false);
m_state.buttons.button_1 = btn.value("button_1", false);
m_state.buttons.button_2 = btn.value("button_2", false);
m_state.buttons.button_3 = btn.value("button_3", false);
}
}
m_state.has_data = true;
}

// Send a command to keep receiving updates (set zero force)
if (!m_state.inverse3_device_id.empty())
{
json cmd;
cmd["inverse3"] = json::array();
json dev;
dev["device_id"] = m_state.inverse3_device_id;
dev["commands"]["set_cursor_force"]["values"]["x"] = 0;
dev["commands"]["set_cursor_force"]["values"]["y"] = 0;
dev["commands"]["set_cursor_force"]["values"]["z"] = 0;
cmd["inverse3"].push_back(dev);
ws.send_text(cmd.dump());
}
}
catch (const json::exception& e)
{
std::cerr << "[HaplyTracker] JSON parse error: " << e.what() << std::endl;
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Lock held during WebSocket send may cause contention.

The m_state_mutex is held (line 548) while sending the zero-force command via ws.send_text() (line 627). If the send blocks or is slow, this could delay get_raw_state() and inject_hand_data() calls on other threads.

♻️ Proposed fix: release lock before sending
             try
             {
                 json j = json::parse(msg);

-                std::lock_guard<std::mutex> lock(m_state_mutex);
+                std::string device_id_copy;
+                {
+                    std::lock_guard<std::mutex> lock(m_state_mutex);

                 // Parse inverse3 array
                 if (j.contains("inverse3") && j["inverse3"].is_array() && !j["inverse3"].empty())
                 {
                     // ... existing parsing code ...
                 }

                 // Parse wireless_verse_grip array
                 if (j.contains("wireless_verse_grip") && j["wireless_verse_grip"].is_array() &&
                     !j["wireless_verse_grip"].empty())
                 {
                     // ... existing parsing code ...
                 }
+                    device_id_copy = m_state.inverse3_device_id;
+                } // lock released

                 // Send a command to keep receiving updates (set zero force)
-                if (!m_state.inverse3_device_id.empty())
+                if (!device_id_copy.empty())
                 {
                     json cmd;
                     // ... build command ...
-                    dev["device_id"] = m_state.inverse3_device_id;
+                    dev["device_id"] = device_id_copy;
                     // ...
                     ws.send_text(cmd.dump());
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/haply/core/haply_hand_tracking_plugin.cpp` around lines 544 -
634, The code holds m_state_mutex while calling ws.send_text which can block; to
fix, capture the needed value (m_state.inverse3_device_id) into a local string
while holding the lock (lock_guard on m_state_mutex), then release the lock and
construct/send the zero-force command with ws.send_text using that local
variable; update the block around m_state.inverse3_device_id and ws.send_text
(referencing m_state_mutex, m_state.inverse3_device_id, and ws.send_text) so no
mutex is held during the send.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant