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: Python/examples/sdk_client.py in the MANUS SDK package
The code snippets in this article are pulled directly from examples/sdk_client.py, the source file is noted in each snippet's title.
Features:
- Interactive menu system for exploring all data streams
- Glove ergonomics data visualization
- Haptic feedback control
- Processed and raw skeleton setup, loading, and visualization
- Raw device (IMU) sensor data stream
- Landscape view
- Glove calibration workflow
- Glove pairing and unpairing
- Tracker management
- User management (add, remove, assign dongles/gloves, reorder)
- Tracking system timeout and per-system settings
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.
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.
Raw Skeleton Callback
def on_raw_skeleton_callback(self, stream_info):
"""Callback for raw skeleton data - caches by glove ID since callbacks return one glove at a time"""
try:
skeletons_count = stream_info.skeletonsCount
next_data = {}
for i in range(skeletons_count):
# Get skeleton info (metadata such as glove ID and node count)
info = ffi.new("RawSkeletonInfo*")
result = lib.CoreSdk_GetRawSkeletonInfo(i, info)
if result != SDKReturnCode.Success:
continue
# Get the skeleton nodes (transforms) for this glove
nodes_array = ffi.new("SkeletonNode[]", info.nodesCount)
result = lib.CoreSdk_GetRawSkeletonData(i, nodes_array, info.nodesCount)
if result != SDKReturnCode.Success:
continue
# Get node info array for this glove (hierarchy and chain information)
node_info_array = ffi.new("NodeInfo[]", info.nodesCount)
result = lib.CoreSdk_GetRawSkeletonNodeInfoArray(info.gloveId, node_info_array, info.nodesCount)
# Make deep copies of the data to avoid CFFI memory reuse issues
skeleton_data = {
'glove_id': info.gloveId,
'nodes_count': info.nodesCount,
'publish_time': info.publishTime,
'nodes': []
}
for j in range(info.nodesCount):
node = nodes_array[j]
node_data = {
'id': node.id,
'position': {
'x': node.transform.position.x,
'y': node.transform.position.y,
'z': node.transform.position.z
},
'rotation': {
'w': node.transform.rotation.w,
'x': node.transform.rotation.x,
'y': node.transform.rotation.y,
'z': node.transform.rotation.z
},
'scale': {
'x': node.transform.scale.x,
'y': node.transform.scale.y,
'z': node.transform.scale.z
}
}
# Add node info if available
if result == SDKReturnCode.Success:
node_info = node_info_array[j]
node_data['node_info'] = {
'node_id': node_info.nodeId,
'parent_id': node_info.parentId,
'chain_type': node_info.chainType,
'side': node_info.side,
'finger_joint_type': node_info.fingerJointType
}
skeleton_data['nodes'].append(node_data)
next_data[info.gloveId] = skeleton_data
# Hand the data over to the main thread instead of processing it here,
# blocking the callback thread would delay incoming data
with self.raw_skeleton_data_mutex:
self.next_raw_skeleton_data = next_data
except Exception as e:
self.log("ERROR", f"Raw skeleton callback error: {e}")
print(f"Error in raw skeleton callback: {e}")
Ergonomics Callback
def on_ergonomics_callback(self, ergonomics_stream):
"""Callback for ergonomics data"""
try:
# Update cached ergonomics data by glove ID
count = ergonomics_stream.dataCount
next_ergonomics_data = {}
for i in range(count):
data = ergonomics_stream.data[i]
glove_id = data.id
# Make a deep copy of the data since CFFI structures may be reused
data_copy = {
'id': data.id,
'isUserID': data.isUserID,
'data': list(data.data) # Convert CFFI array to Python list
}
next_ergonomics_data[glove_id] = data_copy
# Hand the data over to the main thread instead of processing it here
with self.ergonomics_mutex:
self.next_ergonomics_data = next_ergonomics_data
except Exception as e:
self.log("ERROR", f"Ergonomics callback error: {e}")
print(f"Error in ergonomics callback: {e}")
Landscape Callback
def on_landscape_callback(self, landscape):
"""Callback for landscape data"""
try:
# Quick validation before storing - check if counts are reasonable
glove_count = landscape.gloveDevices.gloveCount
dongle_count = landscape.gloveDevices.dongleCount
user_count = landscape.users.userCount
skeleton_count = landscape.skeletons.skeletonCount
tracker_count = landscape.trackers.trackerCount
# Only process if counts are valid
if not (0 <= glove_count <= 32 and
0 <= dongle_count <= 32 and
0 <= user_count <= 10 and
0 <= skeleton_count <= 32 and
0 <= tracker_count <= 32):
return # Skip invalid data
After this validation, the remainder of the function deep-copies the landscape structure into a Python dictionary so the data stays valid outside the callback.
Raw Device Data Callback
def on_raw_device_data_callback(self, raw_device_info):
"""Callback for raw device sensor data"""
try:
raw_device_data_count = raw_device_info.rawDeviceDataCount
# Track callback invocation count
self.raw_device_callback_count = getattr(self, 'raw_device_callback_count', 0) + 1
self.last_raw_device_data_count = raw_device_data_count
next_raw_device_data = {}
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
# Deep copy the sensor transforms (position, rotation, scale)
sensors = []
for sensor_idx in range(sensor_count):
sensor_transform = raw_device.sensorData[sensor_idx]
sensor_data = {
'position': {
'x': sensor_transform.position.x,
'y': sensor_transform.position.y,
'z': sensor_transform.position.z
},
'rotation': {
'w': sensor_transform.rotation.w,
'x': sensor_transform.rotation.x,
'y': sensor_transform.rotation.y,
'z': sensor_transform.rotation.z
},
'scale': {
'x': sensor_transform.scale.x,
'y': sensor_transform.scale.y,
'z': sensor_transform.scale.z
}
}
sensors.append(sensor_data)
# Cache device data by device ID
device_data = {
'id': device_id,
'sensor_count': sensor_count,
'sensors': sensors,
'rotation': {
'w': raw_device.rotation.w,
'x': raw_device.rotation.x,
'y': raw_device.rotation.y,
'z': raw_device.rotation.z
}
}
next_raw_device_data[device_id] = device_data
# Hand the data over to the main thread instead of processing it here
with self.raw_device_data_mutex:
self.next_raw_device_data = next_raw_device_data
except Exception as e:
self.log("ERROR", f"Raw device data callback error: {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):
"""Callback for processed skeleton data"""
try:
skeletons_count = stream_info.skeletonsCount
new_skeletons = []
for i in range(skeletons_count):
# Get skeleton info (skeleton ID and node count)
info = ffi.new("SkeletonInfo*")
result = lib.CoreSdk_GetSkeletonInfo(i, info)
if result != SDKReturnCode.Success:
continue
# Get the skeleton nodes with their animation transforms
nodes_array = ffi.new("SkeletonNode[]", info.nodesCount)
result = lib.CoreSdk_GetSkeletonData(i, nodes_array, info.nodesCount)
if result != SDKReturnCode.Success:
continue
# Make deep copies of the data, the CFFI buffers are reused by the SDK
skeleton_data = {
'skeleton_id': info.id,
'nodes_count': info.nodesCount,
'publish_time': info.publishTime,
'nodes': []
}
for j in range(info.nodesCount):
node = nodes_array[j]
node_data = {
'id': node.id,
'position': {
'x': node.transform.position.x,
'y': node.transform.position.y,
'z': node.transform.position.z
},
'rotation': {
'w': node.transform.rotation.w,
'x': node.transform.rotation.x,
'y': node.transform.rotation.y,
'z': node.transform.rotation.z
},
'scale': {
'x': node.transform.scale.x,
'y': node.transform.scale.y,
'z': node.transform.scale.z
}
}
# Add node_info from the skeleton setup metadata if available
if info.id in self.skeleton_metadata and node.id in self.skeleton_metadata[info.id]:
chain_info = self.skeleton_metadata[info.id][node.id]
node_data['node_info'] = chain_info
skeleton_data['nodes'].append(node_data)
new_skeletons.append(skeleton_data)
# Hand the data over to the main thread instead of processing it here
with self.skeleton_mutex:
self.next_skeletons = new_skeletons
except Exception as e:
self.log("ERROR", f"Skeleton callback error: {e}")
print(f"Error in skeleton callback: {e}")
Tracker Callback
def on_tracker_callback(self, stream_info):
try:
tracker_count = stream_info.trackerCount
if tracker_count == 0:
return
next_tracker_data = {}
for i in range(tracker_count):
data = ffi.new("TrackerData*")
result = lib.CoreSdk_GetTrackerData(i, data)
if result != SDKReturnCode.Success:
continue
tracker_id = ffi.string(data.trackerId.id).decode('utf-8', errors='replace')
next_tracker_data[tracker_id] = {
'tracker_type': int(data.trackerType),
'user_id': int(data.userId),
'is_hmd': bool(data.isHmd),
'position': (data.position.x, data.position.y, data.position.z),
'rotation': (data.rotation.x, data.rotation.y, data.rotation.z, data.rotation.w),
'quality': int(data.quality),
}
with self.tracker_mutex:
self.next_tracker_data = next_tracker_data
self.callback_counter['tracker'] += 1
except Exception as e:
self.log("ERROR", f"Tracker callback error: {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
[T] Trackers
[Y] Tracking Settings
[U] Users
[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 |
| [T] | Trackers menu | T |
| [Y] | Tracking settings menu | Y |
| [U] | Users management menu | U |
| [Q] | Exit the application | Q |
Retrieving a license
The client can retrieve a dongle's newest license online from the MANUS license service. It is reached from the Pairing / Unpairing menu ([P]), which offers [R] Retrieve first dongle's license.
Retrieving the license and writing back the newest signed license:
def retrieve_license(self):
dongles = self.landscape.get('dongles', []) if self.landscape else []
if not dongles:
self.log("WARN", "No dongle available to retrieve a license for")
return
dongle_id = dongles[0]['id']
response = ffi.new("Response*")
result = lib.CoreSdk_RetrieveLicense(dongle_id, response)
if result == SDKReturnCode.Success:
msg = ffi.string(response.message.message).decode('utf-8', errors='replace')
self.log("INFO", f"License retrieved for dongle {dongle_id}: {msg}")
else:
self.log("ERROR", f"Retrieve license failed: {SDKReturnCode(result).name}")
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: create and initialize a NodeSetup cdata value and return it (by-value).
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 (UTF-8, up to 64 bytes)
Returns:
NodeSetup cdata value with all fields initialized:
position at (x, y, z), identity rotation, unit scale,
type Joint and no special settings (IK, Foot, Leaf, etc.)
"""
from manus_sdk.generated._enums import NodeType, NodeSettingsFlag
node = ffi.new("NodeSetup*")
# Manual zero initialization since _Init functions aren't exported
node.id = node_id
node.parentID = parent_id
node.type = NodeType.Joint
node.settings.usedSettings = NodeSettingsFlag.None_
node.transform.position.x = x
node.transform.position.y = y
node.transform.position.z = z
node.transform.rotation.w = 1.0
node.transform.rotation.x = 0.0
node.transform.rotation.y = 0.0
node.transform.rotation.z = 0.0
node.transform.scale.x = 1.0
node.transform.scale.y = 1.0
node.transform.scale.z = 1.0
if name:
name_bytes = name.encode('utf-8')
ffi.memmove(node.name, name_bytes, min(len(name_bytes), 64))
return node[0]
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 simple hand skeleton (root + 5 fingers x 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
"""
# Finger and joint counts
t_NumFingers = 5
t_NumJoints = 4
# Left and right hand node positions
s_LeftHandPositions = [
(0.025320, -0.024950, 0.0),
(0.025320 + 0.032742, -0.024950, 0.0),
(0.025320 + 0.032742 + 0.028739, -0.024950, 0.0),
(0.025320 + 0.032742 + 0.028739 + 0.028739, -0.024950, 0.0),
(0.052904, -0.011181, 0.0),
(0.052904 + 0.038257, -0.011181, 0.0),
(0.052904 + 0.038257 + 0.020884, -0.011181, 0.0),
(0.052904 + 0.038257 + 0.020884 + 0.018759, -0.011181, 0.0),
(0.051287, 0.0, 0.0),
(0.051287 + 0.041861, 0.0, 0.0),
(0.051287 + 0.041861 + 0.024766, 0.0, 0.0),
(0.051287 + 0.041861 + 0.024766 + 0.019683, 0.0, 0.0),
(0.049802, 0.011274, 0.0),
(0.049802 + 0.039736, 0.011274, 0.0),
(0.049802 + 0.039736 + 0.023564, 0.011274, 0.0),
(0.049802 + 0.039736 + 0.023564 + 0.019868, 0.011274, 0.0),
(0.047309, 0.020145, 0.0),
(0.047309 + 0.033175, 0.020145, 0.0),
(0.047309 + 0.033175 + 0.018020, 0.020145, 0.0),
(0.047309 + 0.033175 + 0.018020 + 0.019129, 0.020145, 0.0),
]
s_RightHandPositions = [
(0.025320, 0.024950, 0.0),
(0.025320 + 0.032742, 0.024950, 0.0),
(0.025320 + 0.032742 + 0.028739, 0.024950, 0.0),
(0.025320 + 0.032742 + 0.028739 + 0.028739, 0.024950, 0.0),
(0.052904, 0.011181, 0.0),
(0.052904 + 0.038257, 0.011181, 0.0),
(0.052904 + 0.038257 + 0.020884, 0.011181, 0.0),
(0.052904 + 0.038257 + 0.020884 + 0.018759, 0.011181, 0.0),
(0.051287, 0.0, 0.0),
(0.051287 + 0.041861, 0.0, 0.0),
(0.051287 + 0.041861 + 0.024766, 0.0, 0.0),
(0.051287 + 0.041861 + 0.024766 + 0.019683, 0.0, 0.0),
(0.049802, -0.011274, 0.0),
(0.049802 + 0.039736, -0.011274, 0.0),
(0.049802 + 0.039736 + 0.023564, -0.011274, 0.0),
(0.049802 + 0.039736 + 0.023564 + 0.019868, -0.011274, 0.0),
(0.047309, -0.020145, 0.0),
(0.047309 + 0.033175, -0.020145, 0.0),
(0.047309 + 0.033175 + 0.018020, -0.020145, 0.0),
(0.047309 + 0.033175 + 0.018020 + 0.019129, -0.020145, 0.0),
]
from manus_sdk.generated._enums import Side
fingers = s_LeftHandPositions if side_enum == Side.Left else s_RightHandPositions
# Add root hand node (ID 0)
root_node = self._create_node_setup(0, 0, 0.0, 0.0, 0.0, "Hand")
res = lib.CoreSdk_AddNodeToSkeletonSetup(setup_index, root_node)
if res != SDKReturnCode.Success:
self.log("ERROR", f"Failed to add root Hand node to skeleton setup: {res}")
return False
# Add finger joints
finger_id = 0
for i in range(t_NumFingers):
parent_id = 0
for j in range(t_NumJoints):
idx = i * t_NumJoints + j
x, y, z = fingers[idx]
node_id = 1 + finger_id + j
node = self._create_node_setup(node_id, parent_id, x, y, z, "fingerdigit")
res = lib.CoreSdk_AddNodeToSkeletonSetup(setup_index, node)
if res != SDKReturnCode.Success:
self.log("ERROR", f"Failed to add finger node to skeleton setup: {res}")
return False
parent_id = node_id
finger_id += t_NumJoints
return True
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 hand and finger chains for the 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 the hand chain
- Leaf nodes disabled (chains extend to the finger tips)
"""
from manus_sdk.generated._enums import ChainType, HandMotion
# Hand chain settings
cs = ffi.new("ChainSettings*")
# Manual initialization - set default values
cs.usedSettings = ChainType.Hand
cs.hand.handMotion = HandMotion.IMU
cs.hand.fingerChainIdsUsed = 5
cs.hand.fingerChainIds[0] = 1
cs.hand.fingerChainIds[1] = 2
cs.hand.fingerChainIds[2] = 3
cs.hand.fingerChainIds[3] = 4
cs.hand.fingerChainIds[4] = 5
chain = ffi.new("ChainSetup*")
# Manual initialization
chain.id = 0
chain.type = ChainType.Hand
chain.dataType = ChainType.Hand
chain.side = side_enum
chain.dataIndex = 0
chain.nodeIdCount = 1
chain.nodeIds[0] = 0
chain.settings = cs[0]
res = lib.CoreSdk_AddChainToSkeletonSetup(setup_index, chain[0])
if res != SDKReturnCode.Success:
self.log("ERROR", f"Failed to add Hand chain to skeleton setup: {res}")
return False
# Finger chains
finger_types = [ChainType.FingerThumb, ChainType.FingerIndex, ChainType.FingerMiddle, ChainType.FingerRing, ChainType.FingerPinky]
for i in range(5):
cs2 = ffi.new("ChainSettings*")
# Manual initialization
cs2.usedSettings = finger_types[i]
cs2.finger.handChainId = 0
cs2.finger.metacarpalBoneId = -1
cs2.finger.useLeafAtEnd = False
ch = ffi.new("ChainSetup*")
# Manual initialization
ch.id = i + 1
ch.type = finger_types[i]
ch.dataType = finger_types[i]
ch.side = side_enum
ch.dataIndex = 0
ch.nodeIdCount = 4
if i == 0:
ch.nodeIds[0] = 1
ch.nodeIds[1] = 2
ch.nodeIds[2] = 3
ch.nodeIds[3] = 4
else:
ch.nodeIds[0] = (i * 4) + 1
ch.nodeIds[1] = (i * 4) + 2
ch.nodeIds[2] = (i * 4) + 3
ch.nodeIds[3] = (i * 4) + 4
ch.settings = cs2[0]
res = lib.CoreSdk_AddChainToSkeletonSetup(setup_index, ch[0])
if res != SDKReturnCode.Success:
self.log("ERROR", f"Failed to add finger chain to skeleton setup: {res}")
return False
return True
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 and right 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
Trackers
The Trackers menu provides live visualization of all connected trackers and tools for assigning them to users.
============================================================
TRACKERS
============================================================
[O] Toggle Test Tracker (OFF) [G] Toggle View (Global)
[A] Assign Tracker [U] Unassign Tracker [T] Set Offset
[B] Back to main menu
============================================================
Controls
| Option | Description |
|---|---|
| [O] | Toggle a virtual test tracker sent to MANUS Core |
| [G] | Switch between Global view (all trackers) and Per-User view |
| [A] | Assign the first available tracker to the first user with a Left Hand role |
| [U] | Unassign the first assigned tracker (sets its user ID to 0) |
| [T] | Set a tracker offset for the first user's left-hand tracker |
| [B] | Return to the main menu |
Test Tracker
Toggling the test tracker ([O]) sends a virtual TrackerData to MANUS Core every display cycle. This is useful for testing tracker integration without physical hardware. The test tracker is a Head-type tracker fixed at position (0, 1, 0) with an identity rotation.
Tracker Views
- Global view: Lists every discovered tracker with its ID, type, position, rotation, and tracking quality.
- Per-User view: Groups trackers by the user ID they are currently assigned to.
Users
The Users menu provides tools for managing MANUS Core user accounts and their device assignments.
============================================================
USERS
============================================================
[Z] Disable Auto-Assignment [X] Enable Auto-Assignment
[A] Add User [R] Remove User
[D] Assign Dongle [U] Unassign Dongle
[G] Assign Glove [W] Unassign Glove
[I] Move User Up [K] Move User Down
[N] Change Username
[B] Back to main menu
============================================================
Controls
| Option | Description |
|---|---|
| [Z] | Disable automatic glove-to-user assignment |
| [X] | Enable automatic glove-to-user assignment |
| [A] | Create a new user |
| [R] | Remove the last user in the list |
| [D] | Assign the first available unassigned dongle to a user |
| [U] | Unassign the dongle from the first user that has one assigned |
| [G] | Assign the first available unpaired glove to a user matching the glove's side |
| [W] | Unassign the first assigned glove from its user |
| [I] | Move the user with the highest ID one position up |
| [K] | Move the user with the highest ID one position down |
| [N] | Append " but different" to the first user's name (demonstrates CoreSdk_SetUserName) |
| [B] | Return to the main menu |
Tracking Settings
The Tracking Settings menu exposes controls for tracker timeout behaviour and per-system configuration.
============================================================
TRACKING SETTINGS
============================================================
[T] Toggle Tracker Timeouts
[L] Cycle Timeout Duration (10s / 30s)
[0-5] Open Tracker System | [B] Back
============================================================
Tracker Timeouts: Enabled (Duration: 30.0s)
Controls
| Option | Description |
|---|---|
| [T] | Toggle tracker timeouts on or off |
| [L] | Cycle the timeout duration between 10 s and 30 s |
| [0–5] | Open the submenu for the corresponding tracker system |
| [B] | Return to the main menu |
Tracker System Submenu
Selecting a numbered tracker system opens its settings submenu:
============================================================
TRACKER SYSTEM
============================================================
[A] Toggle Active
[0-9] Select Setting
[B] Back to tracking settings
============================================================
Each tracker system can be enabled or disabled with [A]. Its settings list contains typed values (integer, boolean, IP address, or file path). Select a setting by number to edit it interactively.
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
In the SDK_Client, all callback data is protected using a double-buffer pattern: callbacks write to a next_* staging variable under a mutex, and the main thread swaps the staged data into the live variable in update_before_displaying_data(). This avoids blocking the callback thread during rendering.
import threading
self.raw_skeleton_data_mutex = threading.Lock()
# In callback (SDK thread):
with self.raw_skeleton_data_mutex:
self.next_raw_skeleton_data = next_data # stage new data
# In main thread (update_before_displaying_data):
with self.raw_skeleton_data_mutex:
if self.next_raw_skeleton_data is not None:
self.raw_skeleton_data.update(self.next_raw_skeleton_data)
self.next_raw_skeleton_data = None # consume
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).
Creates a fully configured hand skeleton matching the C++ SDK sample:
- 1 root hand node
- 20 finger joint nodes (5 fingers x 4 joints)
- 6 chains (1 hand chain + 5 finger chains)
Nodes and chains are created before loading the skeleton so callbacks
will see a fully populated hand skeleton.
Args:
side: 'left' or 'right' to specify which hand to load
After loading, the skeleton will:
- Generate skeleton callbacks with animation data
- Be visible in the Skeleton Data view
- Be animated by connected glove data
- Keep streaming until unload_test_skeleton() is called
"""
try:
from manus_sdk.generated._enums import SkeletonType, SkeletonTargetType, Side
# Determine which side
side_enum = Side.Left if side == 'left' else Side.Right
side_name = "Left" if side == 'left' else "Right"
# Create skeleton setup
setup = ffi.new("SkeletonSetupInfo*")
# Manual initialization - zero out and set defaults
setup.type = SkeletonType.Hand
setup.settings.scaleToTarget = True
setup.settings.useEndPointApproximations = True
setup.settings.targetType = SkeletonTargetType.UserIndexData
setup.settings.skeletonTargetUserIndexData.userIndex = 0 # Set skeleton name
name = f"{side_name}Hand"
name_bytes = name.encode('utf-8')
ffi.memmove(setup.name, name_bytes, min(len(name_bytes), 64))
# Create skeleton setup
setup_index_out = ffi.new("uint32_t*")
result = lib.CoreSdk_CreateSkeletonSetup(setup[0], setup_index_out)
if result != SDKReturnCode.Success:
self.log("ERROR", f"Failed to create skeleton setup: {result}")
return
setup_index = setup_index_out[0]
self.temporary_skeletons.append(setup_index)
# Add nodes and chains for a hand skeleton
# Note: functions return False on failure and will cleanup the temporary list below
from manus_sdk.generated._enums import Side as _SideEnum
if not self._setup_hand_nodes(setup_index, side_enum):
if setup_index in self.temporary_skeletons:
self.temporary_skeletons.remove(setup_index)
return
if not self._setup_hand_chains(setup_index, side_enum):
if setup_index in self.temporary_skeletons:
self.temporary_skeletons.remove(setup_index)
return
# Load the skeleton
skeleton_id_out = ffi.new("uint32_t*")
result = lib.CoreSdk_LoadSkeleton(setup_index, skeleton_id_out)
if result != SDKReturnCode.Success:
self.log("ERROR", f"Failed to load skeleton: {result}")
if setup_index in self.temporary_skeletons:
self.temporary_skeletons.remove(setup_index)
return
skeleton_id = skeleton_id_out[0]
if skeleton_id == 0:
self.log("ERROR", "Failed to assign skeleton ID")
if setup_index in self.temporary_skeletons:
self.temporary_skeletons.remove(setup_index)
return
# Build skeleton metadata by reading chains from setup
try:
from manus_sdk.generated._enums import ChainType
# Get the number of chains in the setup
setup_sizes = ffi.new("SkeletonSetupArraySizes*")
result = lib.CoreSdk_GetSkeletonSetupArraySizes(setup_index, setup_sizes)
if result == SDKReturnCode.Success and setup_sizes.chainsCount > 0:
# Allocate array for chains
chains_array = ffi.new("ChainSetup[]", setup_sizes.chainsCount)
result = lib.CoreSdk_GetSkeletonSetupChainsArray(setup_index, chains_array, setup_sizes.chainsCount)
if result == SDKReturnCode.Success:
# Build node -> chain_type mapping
node_to_chain = {}
for chain_idx in range(setup_sizes.chainsCount):
chain = chains_array[chain_idx]
chain_type = chain.type
side = chain.side
# Add each node in this chain to the mapping
for node_idx in range(chain.nodeIdCount):
node_id = chain.nodeIds[node_idx]
node_to_chain[node_id] = {
'chain_type': chain_type,
'side': side,
'node_id': node_id,
'parent_id': 0, # We'd need to look this up from NodeSetup if needed
'finger_joint_type': 0 # Default, would need to look up if needed
}
# Store the metadata for this skeleton
self.skeleton_metadata[skeleton_id] = node_to_chain
except Exception as e:
self.log("WARN", f"Could not build skeleton metadata: {e}")
self.loaded_skeletons.append(skeleton_id)
self.log("INFO", f"Loaded {side_name} hand skeleton (ID: {skeleton_id})")
except Exception as e:
self.log("ERROR", f"Error loading skeleton: {e}")
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 this, no more skeleton callbacks are received for this
skeleton and the Skeleton Data view will be empty.
"""
if not self.loaded_skeletons:
self.log("WARN", "No loaded skeleton to unload")
return
try:
skeleton_id = self.loaded_skeletons[0]
result = lib.CoreSdk_UnloadSkeleton(skeleton_id)
if result == SDKReturnCode.Success:
self.loaded_skeletons.pop(0)
# Clean up metadata
if skeleton_id in self.skeleton_metadata:
del self.skeleton_metadata[skeleton_id]
self.log("INFO", f"Unloaded skeleton (ID: {skeleton_id})")
else:
self.log("ERROR", f"Failed to unload skeleton: {result}")
except Exception as e:
self.log("ERROR", f"Error unloading skeleton: {e}")
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