License requirement
The functionality described requires a MANUS Bodypack or a MANUS license key with the SDK feature enabled.
SDK Client example
Introduction
The SDK Client example functions as a demonstration of all major SDK functions available through Python. It is a menu-driven console application that shows how to connect to MANUS Core, how to set up the coordinate system, and how to handle multiple data streams including gloves, ergonomics, landscape (device inventory), raw skeleton data, and processed skeleton animations. The example client is written in Python and demonstrates best practices for working with the MANUS SDK in an interactive environment.
Location: examples/sdk_client.py
Features:
- Interactive menu system for exploring data
- Glove data stream visualization
- Skeleton setup and management with proper node/chain hierarchies
SDK Callbacks
All the data streams MANUS Core provides are structured as callbacks. A callback is a function that you pass to a CoreSdk_RegisterCallback... function. These functions are called on another thread when data becomes available. This way, applications do not need to poll the SDK to determine if there is new data.
It is good practice to register all the callbacks you require after initializing the SDK and before connecting to MANUS Core. You only need to register the callbacks which you intend to use; if you do not plan to use certain data streams, this could reduce network traffic from MANUS Core.
Raw Skeleton Callback
def on_raw_skeleton_callback(self, stream_info):
"""Raw skeleton data from streaming source"""
try:
skeletons_count = stream_info.skeletonsCount
for i in range(skeletons_count):
# Get skeleton info
info = ffi.new("RawSkeletonInfo*")
result = lib.CoreSdk_GetRawSkeletonInfo(i, info)
if result != SDKReturnCode.Success:
continue
# Get skeleton nodes
nodes_array = ffi.new("SkeletonNode[]", info[0].nodesCount)
result = lib.CoreSdk_GetRawSkeletonData(i, nodes_array, info[0].nodesCount)
if result != SDKReturnCode.Success:
continue
# Cache by glove ID (callbacks may return one glove at a time)
glove_id = info[0].gloveId
self.raw_skeleton_data[glove_id] = {
'nodes_count': info[0].nodesCount,
'nodes': [self._convert_skeleton_node(nodes_array[j])
for j in range(info[0].nodesCount)],
'side': info[0].side
}
except Exception as e:
print(f"Error in raw skeleton callback: {e}")
A best practice for data received on another thread is to save the data to a thread-safe location and process it on your own thread. This way, you do not block the callback thread, which could lead to delayed data transfer.
Ergonomics Callback
def on_ergonomics_callback(self, stream_info):
"""Finger joint angles and bend information"""
try:
for i in range(stream_info.dataCount):
ergo_data = stream_info.data[i]
if ergo_data.isUserID:
continue # Skip user IDs, use glove IDs only
glove_id = ergo_data.id
# Convert CFFI data to Python dict for persistence
self.ergonomics_data[glove_id] = {
'id': ergo_data.id,
'isUserID': ergo_data.isUserID,
'data': [ergo_data.data[j] for j in range(119)] # Copy all values
}
except Exception as e:
print(f"Error in ergonomics callback: {e}")
Landscape Callback
def on_landscape_callback(self, landscape_info):
"""System overview (devices, users, configuration)"""
try:
# Deep copy entire landscape structure
self.landscape_data = self._convert_landscape_to_dict(landscape_info)
except Exception as e:
print(f"Error in landscape callback: {e}")
Raw Device Data Callback
def on_raw_device_data_callback(self, raw_device_info):
"""Raw sensor data from connected devices (accelerometers, gyros, etc.)"""
try:
raw_device_data_count = raw_device_info.rawDeviceDataCount
for i in range(raw_device_data_count):
raw_device = ffi.new("RawDeviceData*")
result = lib.CoreSdk_GetRawDeviceData(i, raw_device)
if result != SDKReturnCode.Success:
continue
device_id = raw_device.id
sensor_count = raw_device.sensorCount
# Extract sensor transforms (position, rotation, scale)
sensors = []
for sensor_idx in range(sensor_count):
sensor_transform = raw_device.sensorData[sensor_idx]
sensors.append({
'position': (sensor_transform.position.x,
sensor_transform.position.y,
sensor_transform.position.z),
'rotation': (sensor_transform.rotation.w,
sensor_transform.rotation.x,
sensor_transform.rotation.y,
sensor_transform.rotation.z)
})
# Cache device data by device ID
self.raw_device_data[device_id] = {
'id': device_id,
'sensor_count': sensor_count,
'sensors': sensors,
'device_rotation': (raw_device.rotation.w,
raw_device.rotation.x,
raw_device.rotation.y,
raw_device.rotation.z)
}
except Exception as e:
print(f"Error in raw device data callback: {e}")
This callback provides access to raw sensor data including accelerometers, gyroscopes, and other motion sensors from connected Manus devices. Each sensor's position and rotation is available for custom processing.
Skeleton Callback
def on_skeleton_callback(self, stream_info):
"""Processed skeleton data with animation transforms"""
try:
skeletons_count = stream_info.skeletonsCount
new_skeletons = []
for i in range(skeletons_count):
# Get skeleton info
info = ffi.new("SkeletonInfo*")
result = lib.CoreSdk_GetSkeletonInfo(i, info)
if result != SDKReturnCode.Success:
continue
# Get skeleton nodes with transforms
nodes_array = ffi.new("SkeletonNode[]", info.nodesCount)
result = lib.CoreSdk_GetSkeletonData(i, nodes_array, info.nodesCount)
if result != SDKReturnCode.Success:
continue
# Store skeleton data with metadata from setup
skeleton_data = {
'skeleton_id': info.id,
'nodes_count': info.nodesCount,
'nodes': [self._convert_skeleton_node(nodes_array[j])
for j in range(info.nodesCount)]
}
new_skeletons.append(skeleton_data)
self.skeletons = new_skeletons
except Exception as e:
print(f"Error in skeleton callback: {e}")
Client Connection
There are three modes to interact with your glove devices: integrated, local, and remote. The integrated method talks directly to the gloves without needing a MANUS Core instance. The local and remote methods talk to a MANUS Core instance which then communicates with the gloves.
On startup, the choice is given between:
Core Integratedmode: The SDK is integrated into the client application.Core Localmode: The SDK will connect to a MANUS Core running locally on this machine.Core Remotemode: The SDK will search and connect to a MANUS Core instance on the network.
When using Remote, the network is scanned, and a list of available MANUS Core instances is returned.
Main Menu
After initialization and connection, the client displays an interactive main menu:
============================================================
MAIN MENU
============================================================
[G] Gloves & Ergonomics Data
[S] Skeleton Data
[R] Raw Skeleton Data
[D] Raw Device Data (Sensors)
[L] Landscape Data
[C] Glove Calibration
[P] Pairing / Unpairing
[K] Skeleton Management
[Q] Quit
============================================================
Menu Navigation
| Option | Description | Hotkey |
|---|---|---|
| [G] | Display glove & ergonomics data | G |
| [S] | Display processed skeleton data | S |
| [R] | Display raw skeleton data | R |
| [D] | Display raw device sensor data | D |
| [L] | Display landscape (system overview) | L |
| [C] | Glove calibration menu | C |
| [P] | Pairing / Unpairing menu | P |
| [K] | Skeleton management (load/unload) | K |
| [B] | Back to main menu | B |
| [H] | Toggle left/right hand | H |
| [D] | Toggle compact/detailed view | D |
| [U] | Unload loaded skeleton | U |
| [Q] | Exit the application | Q |
Skeleton Management
The client includes skeleton setup and management functionality that allows you to create and load hand skeletons with proper node and chain hierarchies.
Loading a Test Skeleton
To load a hand skeleton:
- From the main menu, select [K] for Skeleton Management
- Select [L] to load a left-hand skeleton or [R] for right-hand
- The skeleton will be created with:
- 1 root hand node
- 20 finger joint nodes (5 fingers × 4 joints)
- 6 chains (1 hand + 5 finger chains)
The skeleton will be fully animated with data from the connected gloves and you'll see the processed skeleton data in the [S] Skeleton Data view.
Unloading a Skeleton
From the Skeleton Management menu, select [U] to unload the currently loaded skeleton.
Helper Functions
_create_node_setup()
def _create_node_setup(self, node_id, parent_id, x, y, z, name=""):
"""Helper function to create and initialize a NodeSetup structure.
Args:
node_id: Unique identifier for this node
parent_id: ID of the parent node in the hierarchy
x, y, z: Position coordinates
name: Optional name for the node
Returns:
NodeSetup cdata structure with all fields initialized
"""
Initializes a node with:
- Position, rotation (identity), and scale (1.0)
- Type set to Joint
- No special settings (IK, Foot, etc.)
- Name as UTF-8 string (up to 64 bytes)
_setup_hand_nodes()
def _setup_hand_nodes(self, setup_index, side_enum):
"""Create nodes for a hand skeleton (1 root + 5 fingers × 4 joints).
Args:
setup_index: Skeleton setup index from CoreSdk_CreateSkeletonSetup
side_enum: Side.Left or Side.Right
Returns:
True if successful, False otherwise
Node Structure:
Node 0: Hand (root)
Nodes 1-4: Thumb metacarpal → distal
Nodes 5-8: Index metacarpal → distal
Nodes 9-12: Middle metacarpal → distal
Nodes 13-16: Ring metacarpal → distal
Nodes 17-20: Pinky metacarpal → distal
"""
Creates anatomically accurate node positions based on the C++ SDK sample with proper parent-child relationships.
_setup_hand_chains()
def _setup_hand_chains(self, setup_index, side_enum):
"""Create chains for a hand skeleton (1 hand + 5 finger chains).
Args:
setup_index: Skeleton setup index from CoreSdk_CreateSkeletonSetup
side_enum: Side.Left or Side.Right
Returns:
True if successful, False otherwise
Chain Structure:
Chain 0: Hand (wrist) - contains all finger chain IDs
Chain 1: Thumb - nodes 1-4
Chain 2: Index - nodes 5-8
Chain 3: Middle - nodes 9-12
Chain 4: Ring - nodes 13-16
Chain 5: Pinky - nodes 17-20
Settings:
- Hand motion: IMU (Inertial Measurement Unit)
- All finger chains linked to hand chain
- Leaf nodes disabled (extended to tips)
"""
Sets up proper chain hierarchies and links fingers to the hand chain.
Skeleton Data View
The Skeleton Data view displays processed skeleton information (animates with glove input after loading):
Compact View:
============================================================
SKELETON DATA (Processed)
============================================================
[D] Toggle detailed view | [B] Back to main menu
============================================================
Received 1 skeleton(s):
Skeleton #1:
Skeleton ID: 1
Node Count: 21
Total Nodes: 21
Detailed View (toggle with [D]):
============================================================
SKELETON DATA (Processed)
============================================================
[D] Toggle detailed view | [B] Back to main menu
============================================================
Received 1 skeleton(s):
Skeleton #1:
Skeleton ID: 1
Total Nodes: 21
All Nodes:
Node 0 (Node_0 ): ( 0.000, 0.000, 0.000)
Node 1 (Node_1 ): ( 0.025, -0.025, 0.000)
Node 2 (Node_2 ): ( 0.058, -0.025, 0.000)
Node 3 (Node_3 ): ( 0.080, -0.020, 0.000)
Node 4 (Node_4 ): ( 0.090, 0.000, 0.000)
... [16 more finger joint nodes]
Node 20 (Node_20 ): ( 0.090, 0.030, 0.000)
Data Stream Views
Raw Skeleton Data
The Raw Skeleton view displays joint positions and rotations for one or both hands:
============================================================
RAW SKELETON DATA - LEFT HAND
============================================================
[H] Toggle hand | [D] Toggle view | [B] Back
============================================================
Left Hand Skeleton:
Glove ID: 1234567899 (0x499602D3)
Node Count: 25
Total Nodes: 25
Controls:
- [H]: Switch between left (glove ID 1) and right (glove ID 2) hand display
- [D]: Toggle between compact (hand structure summary) and detailed view (all nodes with transforms)
- [B]: Return to main menu
Detailed View example:
Raw Skeleton - Detailed View:
Left Hand Skeleton:
Glove ID: 1234567899 (0x499602D3)
Node Count: 25
Total Nodes: 25
All Nodes:
Node 0 (L_Hand): ( 0.000, 0.000, 0.000)
Node 1 (L_Thumb_Metacarpal): ( 0.025, -0.025, 0.000)
Node 2 (L_Thumb_Proximal): ( 0.058, -0.025, 0.000)
Node 3 (L_Thumb_Intermediate): ( 0.080, -0.020, 0.000)
Node 4 (L_Thumb_Distal): ( 0.090, 0.000, 0.000)
... [21 more nodes with positions and rotations]
Node 24 (L_Pinky_Distal): ( 0.090, 0.030, 0.000)
Ergonomics Data
The Ergonomics view displays finger joint bend and spread angles from connected gloves:
============================================================
GLOVES & ERGONOMICS DATA
============================================================
Haptics (HOLD): [1-5] Left (pinky-thumb) [6-0] Right (thumb-pinky)
============================================================
Left Glove:
Glove ID: 2941338881 (0xAF514501)
Joint Angles (degrees):
Thumb : Spread=-36.68° MCP= 44.20° PIP= 0.86° DIP= 30.91°
Index : Spread= 50.52° MCP=-76.32° PIP= 53.92° DIP=113.67°
Middle: Spread= 49.25° MCP=-49.85° PIP= 63.82° DIP= 89.94°
Ring : Spread= 10.89° MCP=-30.96° PIP= 1.79° DIP= 60.08°
Pinky : Spread= 10.44° MCP=-25.79° PIP= 0.62° DIP= 15.35°
Right Glove:
Glove ID: 2941338882 (0xAF514502)
Joint Angles (degrees):
Thumb : Spread= 35.12° MCP= 42.85° PIP= 1.23° DIP= 32.15°
Index : Spread= 48.67° MCP=-75.45° PIP= 55.33° DIP=115.22°
Middle: Spread= 47.89° MCP=-48.92° PIP= 64.15° DIP= 91.28°
Ring : Spread= 12.34° MCP=-32.11° PIP= 2.56° DIP= 61.45°
Pinky : Spread= 9.78° MCP=-26.34° PIP= 0.89° DIP= 16.72°
Data Explanation:
- Glove ID: Hardware identifier (shown in decimal and hexadecimal)
- Spread: Finger abduction/adduction angle from palm center (negative = towards palm)
- MCP: Metacarpophalangeal joint bend (knuckle) in degrees
- PIP: Proximal interphalangeal joint bend (middle joint) in degrees
- DIP: Distal interphalangeal joint bend (tip joint) in degrees
All angles are in degrees with real-time updates at ~100 Hz from connected gloves.
Controls:
- [B]: Return to main menu
Landscape Data
The Landscape view displays the system overview showing all connected devices:
============================================================
LANDSCAPE DATA - System Overview (COMPACT)
============================================================
[D] Toggle detailed view | [B] Back to main menu
============================================================
╔═══ Landscape ═══
║
╠═ Devices
║ ├─ Dongles: 2
║ │ • ID:0x39BFB011 FW:5.16.0
║ │ • ID:0x39BFBFFF FW:5.16.0
║ ├─ Gloves: 2
║ │ • ID:1 (Left) - Dongle:0
║ │ • ID:2 (Right) - Dongle:1
║
║
╠─ Users: 1
║ • WiredUser_0x0000000 (ID:0) Gloves:[L-R]
║
╠─ Skeletons: 1
║ • Skeleton 1 (Hand) User:0
║
╠═ Trackers: 0
║
└─ Settings
• Manus Core: v3.1.1
• Mode: Live
• Max Glove Pairs: 2
• Features: SDK, Recording, Exporting
Compact View provides:
- Dongle count and firmware versions
- Connected glove list with IDs and assignments
- Tracker inventory
- User count
- Skeleton count
Detailed View expands to show all device fields and properties for deep inspection.
Controls:
- [D]: Toggle to detailed view (shows all fields)
- [B]: Return to main menu
Raw Device Data
The Raw Device Data view displays sensor information from connected Manus devices (IMUs, accelerometers, gyroscopes, etc.):
============================================================
RAW DEVICE DATA - Sensor Information
============================================================
[B] Back to main menu
============================================================
Connected Devices: 2
Device ID: 0x39BFB011
Sensor Count: 2
Sensors:
Sensor 1:
Position: ( 0.000, 0.000, 0.000)
Rotation: (W: 1.000, X: 0.000, Y: 0.000, Z: 0.000)
Sensor 2:
Position: ( 0.050, 0.000, 0.000)
Rotation: (W: 0.999, X: 0.045, Y: 0.000, Z: 0.000)
Device Rotation: (W: 0.998, X:-0.063, Y: 0.000, Z: 0.000)
Device ID: 0x39BFBFFF
Sensor Count: 2
Sensors:
Sensor 1:
Position: ( 0.000, 0.000, 0.000)
Rotation: (W: 1.000, X: 0.000, Y: 0.000, Z: 0.000)
Sensor 2:
Position: ( 0.050, 0.000, 0.000)
Rotation: (W: 0.998, X:-0.063, Y: 0.000, Z: 0.000)
Device Rotation: (W: 0.999, X: 0.031, Y: 0.000, Z: 0.000)
Data Displayed:
- Device ID: Hardware identifier for each device
- Sensor Count: Number of motion sensors in the device
- Sensor Position/Rotation: Transform of each sensor relative to device origin
- Device Rotation: Overall device orientation
Use Cases:
- Direct access to raw IMU data for custom motion processing
- Sensor fusion and filtering
- Advanced hand tracking algorithms
- Motion capture and animation
Controls:
- [B]: Return to main menu
Architecture
Skeleton Setup Flow
The skeleton loading process follows this sequence:
- Create Skeleton Setup →
CoreSdk_CreateSkeletonSetup()returns setup index - Add Hand Nodes →
_setup_hand_nodes()adds 21 nodes with positions and hierarchy - Add Hand Chains →
_setup_hand_chains()creates chain topology and links - Load Skeleton →
CoreSdk_LoadSkeleton()returns skeleton ID for activation - Build Metadata → Extract chains to build node→chain_type mapping
- Start Streaming → Skeleton is now animated and generates callbacks
Data Caching
All callback data is cached by glove ID or converted to Python dictionaries to prevent CFFI memory reuse issues:
# Raw skeleton cached by glove ID
self.raw_skeleton_data = {
1: {'nodes_count': 43, 'nodes': [...]}, # Left glove
2: {'nodes_count': 43, 'nodes': [...]} # Right glove
}
# Ergonomics cached by glove ID
self.ergonomics_data = {
1: {'id': 1, 'data': [...]},
2: {'id': 2, 'data': [...]}
}
# Landscape is a deep copy of entire structure
self.landscape_data = {
'dongles': [...],
'gloves': [...],
'users': [...],
...
}
This caching strategy ensures data persists even when CFFI structures are reused by the SDK.
Thread Safety
All callback data is protected by thread-safe primitives:
import threading
self.skeleton_mutex = threading.Lock()
self.ergonomics_mutex = threading.Lock()
self.landscape_mutex = threading.Lock()
# In callback:
with self.skeleton_mutex:
self.raw_skeleton_data[glove_id] = converted_data
# In main thread:
with self.skeleton_mutex:
display_data = self.raw_skeleton_data.copy()
Performance Considerations
| Aspect | Implementation | Notes |
|---|---|---|
| Frame Rate | 10 FPS limit | Reduces terminal flicker glove data rate is much higher |
| Data Copying | Deep CFFI→dict | Prevents memory reuse issues, uses more memory |
| Thread Safety | Mutex per data stream | Callbacks run in SDK threads |
| Memory | Dictionary caching | Requires more memory than direct CFFI use |
Data Structures
ClientRawSkeleton
Container for skeleton callback data:
skeleton_data = {
'nodes_count': 43,
'side': 1, # 1=Left, 2=Right
'nodes': [
{
'transform': {
'position': {'x': 0.0, 'y': 0.05, 'z': -0.005},
'rotation': {'x': 0.0, 'y': 0.087, 'z': -0.005, 'w': 0.996}
}
},
# ... more nodes
]
}
Ergonomics Data
The data array contains: - Spread, MCP, PIP, DIP for each finger (5 fingers × 4 values = 20 values) - Additional sensor data and metrics (remaining ~99 values)
Landscape Data
landscape_data = {
'dongles': [
{'id': 0x39BFB011, 'firmware': '5.16.0', ...}
],
'gloves': [
{'id': 1, 'side': 1, 'dongle_id': 0, ...},
{'id': 2, 'side': 2, 'dongle_id': 1, ...}
],
'users': [],
'skeletons': [
{'id': 1, ...},
{'id': 2, ...}
],
'settings': {...}
}
Customization Examples
Change Frame Rate
# In __init__:
self.min_display_interval = 0.05 # 20 FPS
# or
self.min_display_interval = 0.033 # 30 FPS
Export Data to File
import json
def export_snapshot(self):
"""Export current data snapshot"""
snapshot = {
'timestamp': time.time(),
'skeleton': self.raw_skeleton_data,
'ergonomics': self.ergonomics_data,
'landscape': self.landscape_data
}
with open('sdk_data.json', 'w') as f:
json.dump(snapshot, f, indent=2)
print("Data exported to sdk_data.json")
Connection Flow
The client initializes in the following order:
- Prompt user for connection mode (Integrated/Local/Remote)
- Initialize SDK (
CoreSdk_InitializeIntegratedorCoreSdk_InitializeCore) - Register all callbacks
- Set coordinate system
- Search for hosts and establish connection
- Enter main menu
If connection fails in Local or Remote modes, the client retries every second until successful.
Troubleshooting
"Could not connect" Loop
- Local mode: Ensure MANUS Core is running (
localhost) - Remote mode: Check network connectivity and firewall
- Integrated mode: Should always work; check console for errors
Data Not Updating
- Ensure gloves are powered on and connected
- Verify MANUS Core Dashboard shows glove status
- Check that callbacks are registered before connecting
- Verify gloves are actively sending data
Terminal Display Issues
- Adjust
min_display_intervalif display updates too frequently/slowly - If terminal appears garbled, use full detailed view instead of compact
- Try reducing other terminal output during operation
Remote Mode Takes Long Time
- Network discovery takes ~3 seconds by design
- If no hosts found after 3 seconds, check:
- MANUS Core running on remote machine
- Network connectivity between machines
Skeleton Not Appearing in Skeleton Data
- Ensure you've loaded a skeleton using [K] → [L] or [R]
- Wait a moment for the first skeleton callback to arrive
- Verify raw skeleton data is available first (indicates glove connectivity)
API Reference
load_test_skeleton(side)
Loads a complete hand skeleton with proper node and chain hierarchies.
def load_test_skeleton(self, side):
"""Load a test skeleton for the specified side (left or right).
This function creates a fully configured hand skeleton matching the
C++ SDK sample implementation:
- 1 root hand node
- 20 finger joint nodes (5 fingers × 4 joints)
- 6 chains (1 hand chain + 5 finger chains)
Args:
side: 'left' or 'right' to specify which hand to load
Raises:
Various SDK return code errors if setup fails
After loading, the skeleton will:
- Generate skeleton callbacks with animation data
- Be visible in the Skeleton Data view
- Be animated by connected glove data
- Continue streaming until unload_test_skeleton() is called
Example:
client.load_test_skeleton('left') # Load left hand
# ... wait for callbacks ...
client.unload_test_skeleton() # Unload it
"""
Return Values:
- Logs "Loaded [Side] hand skeleton (ID: X)" on success
- Logs error message on failure with specific SDK return code
Common Issues:
Failed to create skeleton setup: SDK initialization problemFailed to add [object] to skeleton setup: Corrupted node/chain dataFailed to load skeleton: Setup data invalid or SDK issueFailed to assign skeleton ID: Serious SDK state issue
unload_test_skeleton()
Unloads the currently loaded test skeleton.
def unload_test_skeleton(self):
"""Unload the first loaded skeleton from the system.
Removes the skeleton from the animation pipeline and cleans up:
- Skeleton data streaming
- Internal metadata cache
- Resources allocated by the SDK
After calling, no more skeleton callbacks will be received
for this skeleton and the Skeleton Data view will be empty.
Example:
client.unload_test_skeleton() # Remove currently loaded skeleton
"""
Return Values:
- Logs "Unloaded skeleton (ID: X)" on success
- Logs "No loaded skeleton to unload" if none are loaded
- Logs error message on SDK failure
Internal Helper Functions
These are used internally by load_test_skeleton and are documented for reference:
_create_node_setup(node_id, parent_id, x, y, z, name="")
Creates a properly initialized NodeSetup structure with anatomically correct defaults:
- Position at (x, y, z)
- Identity rotation (w=1.0, x/y/z=0.0)
- Unit scale (1.0, 1.0, 1.0)
- Type: Joint
- No special settings (IK, Foot, Leaf, etc.)
_setup_hand_nodes(setup_index, side_enum)
Adds 21 nodes to a skeleton setup:
- 1 root hand node at origin
- 5 fingers with 4 joints each
- Positions based on average hand dimensions from C++ sample
- Proper parent-child hierarchy
_setup_hand_chains(setup_index, side_enum)
Creates 6 chains linking the nodes:
- Hand chain (wrist) containing all finger chain IDs
- 5 finger chains with proper joint sequencing
- Hand motion set to IMU (Inertial Measurement Unit)
- All fingers linked to hand chain