Model packagesHand Algorithm Package

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.

Latest documentation

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 -> Application

The 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 caseSDK topicReturn typePrimary data
Standard robotic-hand retargetingdevice.output()HandJointCommandTarget-hand joint names and joint angles
Dynamic OpenXR skeleton posedevice.skeleton().pose()HandSkeletonPosePositions 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]
FieldDescription
targetIdentifies the target hand for this frame and distinguishes multiple output targets
jointsMaps joint names to joint positions in the form {joint_name: angle_rad}
timestampAlgorithm input/output timestamp in seconds
sourceIdentifies the result source; may be None when no source information is available
metadataAdditional 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_order in 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_ or right_ 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 1Dynamic OpenXR 25-point skeleton pose output example 1
  • Dynamic OpenXR 25-point skeleton pose output example 2Dynamic 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], ...]
FieldDescription
timestamp_secSkeleton-pose timestamp in seconds
hand_sideSide.LEFT or Side.RIGHT
coordinate_frameAlways wrist_local
namesA fixed ordered list of 25 joint names, shown below
positions_localXYZ positions of the 25 joints in the wrist-local coordinate frame, in meters
orientations_local_xyzwUnit 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:

IndexJoint nameDescription
0wristWrist reference joint and local-coordinate origin
1–4thumb_metacarpal through thumb_tipThumb metacarpal, proximal, distal, and tip joints
5–9index_metacarpal through index_tipIndex metacarpal, proximal, intermediate, distal, and tip joints
10–14middle_metacarpal through middle_tipMiddle metacarpal, proximal, intermediate, distal, and tip joints
15–19ring_metacarpal through ring_tipRing metacarpal, proximal, intermediate, distal, and tip joints
20–24little_metacarpal through little_tipLittle-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 namesHand 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 wrist position is fixed at (0, 0, 0).
  • The wrist orientation 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_node

Coordinate-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)