EMF & Hand Tracking

EMF Poses

sub = glove.emf_poses().subscribe()
poses = await sub.recv_async()
for pose in poses.poses:
    pos = pose.pose.position
    print(f"Position: [{pos[0]:+.3f}, {pos[1]:+.3f}, {pos[2]:+.3f}], confidence: {pose.confidence:.2f}")

EMF poses are expressed in the emf_tx (back-of-hand transmitter coil) frame, with frame_id set to l_hand_emf_tx / r_hand_emf_tx. This differs from the wrist frame used by the hand tracking products — see Coordinate Frames for the transform between them.

EmfPose

Single-finger EMF pose.

FieldTypeDescription
posePosePose (position + orientation)
confidencefloatPositioning confidence (0.0~1.0, higher is more reliable. Values below 0.9 can be treated as unreliable)

EmfPoseArray

5-finger EMF pose array. Finger order: thumb, index, middle, ring, pinky.

FieldTypeDescription
headerFrameHeaderFrame header
posesList[EmfPose]5-finger pose list
{
  "header": {
    "seq": 100,
    "timestamp_us": 1709876543210,
    "frame_id": "l_hand_emf_tx"
  },
  "poses": [
    {
      "pose": {
        "position": [0.041, 0.066, 0.022],
        "orientation": {
          "x": 0.0,
          "y": 0.0,
          "z": 0.0,
          "w": 1.0
        }
      },
      "confidence": 0.95
    }
  ]
}

Hand Tracking Products

# Joint angles
sub = glove.hand_joint_angles().subscribe()
angles = await sub.recv_async()
for i, finger in enumerate(angles.fingers):
    print(f"Finger {i}: angles={finger.angles}, confidence={finger.confidence:.2f}")

# Hand skeleton
sub = glove.hand_skeleton().subscribe()
skeleton = await sub.recv_async()
for joint in skeleton.joints:
    print(f"{joint.name}: position={joint.pose.position}")

Hand tracking products (joint angles hand_joint_angles, skeleton hand_skeleton, and fingertip poses tip_poses) are computed from EMF pose frames, and their timestamps match the source EMF frame — use this to align the products with their source data. See the SDK Time Synchronization page for full timestamp semantics.

For skeleton and fingertip poses, frame_id is the wrist frame (l_wrist / r_wrist).

The fingertip-pose seq is an independent stream counter that does not continue the source EMF seq.

FingertipPose

Single-finger fingertip pose.

FieldTypeDescription
posePosePose
confidencefloatConfidence

FingertipPoses

5-finger fingertip poses.

FieldTypeDescription
headerFrameHeaderFrame header
posesList[FingertipPose]5-finger fingertip poses
{
  "header": {
    "seq": 100,
    "timestamp_us": 1709876543210,
    "frame_id": "l_wrist"
  },
  "poses": [
    {
      "pose": {
        "position": [0.041, 0.066, 0.022],
        "orientation": {
          "x": 0.0,
          "y": 0.0,
          "z": 0.0,
          "w": 1.0
        }
      },
      "confidence": 0.95
    },
    ... // 5 fingers total
  ]
}

FingerJointAngles

Single-finger joint angles.

FieldTypeDescription
anglesList[float]Fixed array of 5 joint-angle slots (radians), accessed by index. The thumb uses all 5 values. The other four fingers use only the first 4, with the 5th value fixed at 0 as padding
confidencefloatIK solver confidence

HandJointAngles

Full-hand 21 DoF joint angles, where 21 DoF refers to the degrees of freedom of the hand kinematic model used by the hand tracking algorithm.

Returned per finger. The thumb maps to CMC3 + MCP1 + IP1, so all 5 slots of angles are used. The other four fingers map to MCP2 + PIP1 + DIP1, so only the first 4 slots are used, with the 5th slot as padding. Total 21 DoF for the whole hand. To keep the data structure uniform, the SDK returns a fixed-length 5 angles array per finger, so fingers contains 5 × 5 slots in total.

FieldTypeDescription
headerFrameHeaderFrame header. frame_id carries over from the source EMF frame and has no meaning for joint angles
fingersList[FingerJointAngles]5-finger joint angles in thumb, index, middle, ring, pinky order
{
  "header": {
    "seq": 100,
    "timestamp_us": 1709876543210,
    "frame_id": "l_hand_emf_tx"
  },
  "fingers": [
    {
      "angles": [0.12, 0.34, 0.56, 0.78, 0.91],
      "confidence": 0.92
    },
    {
      "angles": [0.11, 0.22, 0.33, 0.44, 0.0],
      "confidence": 0.88
    },
    ... // 5 fingers total. For non-thumb fingers, the 5th value is 0 padding
  ]
}

SkeletonJoint

Hand skeleton keypoint.

FieldTypeDescription
namestrKeypoint name (such as index_finger_mcp)
posePosePose
confidencefloatConfidence

HandSkeleton

21-keypoint hand skeleton in the MediaPipe topology.

FieldTypeDescription
headerFrameHeaderFrame header
jointsList[SkeletonJoint]21 keypoints
{
  "header": {
    "seq": 100,
    "timestamp_us": 1709876543210,
    "frame_id": "l_wrist"
  },
  "joints": [
    {
      "name": "wrist",
      "pose": {
        "position": [0.0, 0.0, 0.0],
        "orientation": {
          "x": 0.0,
          "y": 0.0,
          "z": 0.0,
          "w": 1.0
        }
      },
      "confidence": 0.95
    },
    ... // 21 MediaPipe keypoints total
  ]
}

Output Rate Tuning

emf_poses publishes at the EMF input rate (120 Hz) by default. To save bandwidth and IK/FK compute, call set_rate(hz) on an emf_poses subscription handle to request a lower output rate. Upgrade the device firmware to v0.11.4 or later and the Wuji SDK to v2026.8.17 or later first.

Calling

sub = glove.emf_poses().subscribe()
actual = sub.set_rate(30)   # request 30 Hz, returns the rate the device applies
sub.set_rate(0)             # restore the default output rate
  • The device quantizes the request to a rate it supports. set_rate() returns the rate in effect.
  • Pass 0 to restore the default output rate.
  • The rate belongs to the device stream and applies to every subscriber of that stream. When several subscribers send requests, the last write wins.
  • The rate resets to full speed when all subscriptions to the stream close or the device reconnects. The setting doesn't persist across connections.
  • Hold an active subscription before calling set_rate(). Without one, the SDK rejects the request locally and sends nothing to the device.

Affected Streams

Lowering emf_poses also lowers the EMF input rate used for hand tracking. The derived streams tip_poses, hand_joint_angles, hand_skeleton, and tactile_point_cloud follow the source rate and can't be tuned on their own — calling set_rate() on a derived-stream handle raises an error.

Four groups of streams are tunable: EMF poses (emf_poses), the tactile frame (tactile), tactile zones (tactile_zones), and the six raw IMU streams (imu_palm, imu_thumb, imu_index, imu_middle, imu_ring, imu_pinky), each tuned independently. Global resources such as tf and tf_static don't support rate tuning.

Interaction with Calibration

When you call glove.calibrate() or calibrate_blocking(), the SDK runs the EMF chain at full speed during capture and restores the previous output rate when capture ends — whether it succeeds, fails, or is canceled. Lowering the emf_poses rate beforehand doesn't stretch the calibration time. See Calibration.

Subscribe to Updates