Qnbot Hand Algorithm Package Output
Understand the output types, fields, and coordinate conventions for robotic-hand joint commands and the Dynamic OpenXR 25-joint skeleton pose.
Qnbot Hand Algorithm Package Output
SDK overview
qnbot-sdk sits between glove data and the hand algorithm package:
Glove data -> qnbot-sdk -> Hand algorithm package -> SDK output topic -> ApplicationThe SDK sends glove data to the selected algorithm package and converts the results into stable public Python data types. Applications subscribe to topics and receive Sample[T] values:
Sample.sequence: the frame sequence number for the current topic, which can be used to detect dropped or out-of-order frames.Sample.value: the algorithm result for the current frame.
Algorithm outputs fall into two categories:
| Use case | SDK topic | Return type | Primary data |
|---|---|---|---|
| Standard robotic-hand retargeting | device.output() | HandJointCommand | Target-hand joint names and joint angles |
| Dynamic OpenXR skeleton pose | device.skeleton().pose() | HandSkeletonPose | Positions and orientations for a fixed set of 25 skeleton joints |
Interface note: Standard robotic hands return joint angles only through device.output(). This interface does not return 3D joint coordinates for the robotic hand. The 25-point coordinate output is specific to the Dynamic OpenXR human-hand skeleton.
Standard robotic-hand output
Return type
The output type for a standard robotic-hand algorithm package is HandJointCommand:
@dataclass(frozen=True)
class HandJointCommand:
target: str
joints: dict[str, float]
timestamp: float
source: str | None
metadata: dict[str, object]| Field | Description |
|---|---|
target | Identifies the target hand for this frame and distinguishes multiple output targets |
joints | Maps joint names to joint positions in the form {joint_name: angle_rad} |
timestamp | Algorithm input/output timestamp in seconds |
source | Identifies the result source; may be None when no source information is available |
metadata | Additional SDK or algorithm information; applications must not assume that an optional key is always present |
Every value in joints is a joint position measured in radians (rad). It is not measured in degrees, and it is not an encoder value, velocity, torque, or current.
Example result:
HandJointCommand(
target="primary",
joints={
"thumb_joint": 0.42,
"index_joint": 0.86,
"middle_joint": 0.74,
},
timestamp=123.456,
source="retargeting",
metadata={},
)The joint names above demonstrate the data structure only. They are not the fixed joint list for a specific robotic hand.
Joint names and counts
Joint names and counts are defined by the algorithm package for the selected target-hand model and therefore differ between robotic hands. The joints dictionary returned by the SDK is the actual public output set for the current target.
Follow these rules when consuming it:
- Always access values by joint name. Do not assume that different hand models share array indices.
- Do not infer the robotic-hand controller order from Python dictionary iteration order.
- When converting the result to a controller array, maintain an explicit
controller_joint_orderin the application and assemble the array by name. - Some hand models contain mechanically coupled or passive joints. An algorithm package may return both active joints and passive joints derived from coupling relationships; do not infer degrees of freedom from the joint count alone.
- Whether left and right hands use different joint-name prefixes depends on the specific model. Use the names returned by the SDK rather than constructing
left_orright_prefixes yourself.
Safety note: Output values have passed through the algorithm package's joint limits and enabled output processing. The application must still perform its own control-safety checks before sending values to physical hardware.
Minimal subscription example
The following example shows only the core output subscription. Device and target configuration are owned by the host application:
from qnbot_sdk import Sample
from qnbot_sdk.glove import HandJointCommand
def on_output(sample: Sample[HandJointCommand]) -> None:
command = sample.value
print("sequence:", sample.sequence)
print("target:", command.target)
for joint_name, angle_rad in command.joints.items():
print(joint_name, angle_rad)
output_subscription = device.output().subscribe(on_output)Dynamic OpenXR output
Dynamic OpenXR provides both joint angles and a 25-point skeleton pose. The two outputs come from the same algorithm package but use different SDK topics; their indices cannot be mapped directly to one another.
Dynamic OpenXR 25-point skeleton pose output example 1
Dynamic OpenXR 25-point skeleton pose output example 2
25-point skeleton pose
Subscribe with:
skeleton = device.skeleton()
skeleton.pose().subscribe(on_skeleton_pose)The return type is HandSkeletonPose:
@dataclass(frozen=True)
class HandSkeletonPose:
timestamp_sec: float
hand_side: Side
coordinate_frame: str
names: tuple[str, ...]
positions_local: tuple[tuple[float, float, float], ...]
orientations_local_xyzw: tuple[tuple[float, float, float, float], ...]| Field | Description |
|---|---|
timestamp_sec | Skeleton-pose timestamp in seconds |
hand_side | Side.LEFT or Side.RIGHT |
coordinate_frame | Always wrist_local |
names | A fixed ordered list of 25 joint names, shown below |
positions_local | XYZ positions of the 25 joints in the wrist-local coordinate frame, in meters |
orientations_local_xyzw | Unit quaternions of the 25 joints relative to the wrist, in XYZW component order |
Elements at the same index in all three arrays describe the same joint:
for index, name in enumerate(pose.names):
position_m = pose.positions_local[index]
orientation_xyzw = pose.orientations_local_xyzw[index]The fixed joint order is:
| Index | Joint name | Description |
|---|---|---|
| 0 | wrist | Wrist reference joint and local-coordinate origin |
| 1–4 | thumb_metacarpal through thumb_tip | Thumb metacarpal, proximal, distal, and tip joints |
| 5–9 | index_metacarpal through index_tip | Index metacarpal, proximal, intermediate, distal, and tip joints |
| 10–14 | middle_metacarpal through middle_tip | Middle metacarpal, proximal, intermediate, distal, and tip joints |
| 15–19 | ring_metacarpal through ring_tip | Ring metacarpal, proximal, intermediate, distal, and tip joints |
| 20–24 | little_metacarpal through little_tip | Little-finger metacarpal, proximal, intermediate, distal, and tip joints |
Complete name order:
wrist
thumb_metacarpal, thumb_proximal, thumb_distal, thumb_tip
index_metacarpal, index_proximal, index_intermediate, index_distal, index_tip
middle_metacarpal, middle_proximal, middle_intermediate, middle_distal, middle_tip
ring_metacarpal, ring_proximal, ring_intermediate, ring_distal, ring_tip
little_metacarpal, little_proximal, little_intermediate, little_distal, little_tip
Hand skeleton diagram: fixed Dynamic OpenXR 25-joint names
Coordinate conventions
wrist_local is a local right-handed Cartesian coordinate frame that moves with the wrist:
- All positions are in meters and use
(x, y, z)component order. - All quaternions use
(x, y, z, w)component order. - The
wristposition is fixed at(0, 0, 0). - The
wristorientation is fixed at(0, 0, 0, 1). - Every other joint position is expressed in the same wrist frame, not as an offset from its parent joint.
- This interface does not provide a world pose.
The coordinate transform is:
p_local = R_wrist^T * (p_node - p_wrist)
R_local = R_wrist^T * R_nodeCoordinate-frame note: If an application needs robot, camera, ROS, or world coordinates, maintain an explicit rigid transform from the wrist frame to the target frame. Do not guess the relationship by swapping XYZ axes, negating one hand, or inspecting an algorithm package's internal URDF.
Minimal subscription example
from qnbot_sdk import Sample
from qnbot_sdk.glove import HandSkeletonPose
def on_pose(sample: Sample[HandSkeletonPose]) -> None:
pose = sample.value
for index, name in enumerate(pose.names):
print(
name,
pose.positions_local[index],
pose.orientations_local_xyzw[index],
)
skeleton = device.skeleton()
pose_subscription = skeleton.pose().subscribe(on_pose)