Joint States and Control
SDK Reference
The whole-hand API covers 20 joints. Single-joint reads and actions go through the JointHandle returned by hand.joint(k) / hand.joints() — see Joint Traversal.
joint_states (SUB)
Joint state stream: subscribe to the real-time state frames of the 20 joints, at a native output rate of 1000 Hz. Lower the push rate through the subscription handle: sub.set_rate(hz) returns the rate in effect, and 0 restores the native rate — see Adjusting the Stream Output Rate for full usage. This stream and joint_diagnostics ride the same device stream, so lowering the rate of one lowers both.
Push-rate tuning applies to the joint_states, joint_diagnostics, and imu streams. Upgrade the firmware and the Wuji SDK together to versions that support it — see the release notes for the version requirements.
sub = hand.joint_states().subscribe() # → Subscription[JointStateFrame]
frame = sub.recv() # synchronous, non-blocking; None = no data yet
frame = await sub.recv_async() # asynchronous wait
print(frame.header.seq, frame.header.timestamp_us)
for j in frame.joints:
print(j.nid, j.position, j.velocity, j.effort)
sub.close() Callback mode:
def on_state(frame):
print(frame.header.seq, [j.position for j in frame.joints[:4]])
cb = hand.joint_states().subscribe_with_callback(on_state)
# ...
cb.close() position is the joint-side angle (rad) and velocity the joint-side angular velocity (rad/s)—this is the only recommended path for joint angle and speed. Frames are variable-length and contain only online joints — identify each entry by its nid. The joint_diagnostics stream doesn’t carry joint angles.
joint_command (PUB)
Joint command: publish positions / velocities / torque feedforward for the 20 joints. Each send takes exactly 20 JointCommand entries, one position / velocity / effort per joint.
from wuji_sdk import JointCommand
pub = hand.joint_command().publish() # → JointCommandPublisher
pub.send([JointCommand(position=p, velocity=0.0, effort=0.0) for p in positions])
pub.close() To turn human hand keypoints into these 20-joint commands, use Wuji SDK Hand Retargeting.
online_joints_count (GET)
Get the online joint count (0–20).
n = hand.online_joints_count().get() # → int, 0–20 Joint Traversal
JointHandle provides label / index (for indexing into 20-element return arrays such as effort_limit() / mit_params()), plus single-joint resources and actions. FingerHandle traverses joints by finger.
| Method | Return type | Description |
|---|---|---|
hand.joints() | list[JointHandle] | All 20 joints |
hand.fingers() | list[FingerHandle] | All 5 fingers |
for joint in hand.joints():
print(joint.label, joint.index) # e.g. "thumb_S1" 0
for finger in hand.fingers():
for joint in finger.joints(): # 4 joints per finger, in S1..S4 order
print(joint.label) # "thumb_S1", "thumb_S2", ... JointHandle
Joint handle: provides label / index, single-joint resources, and single-joint actions.
| Member | Type | Description |
|---|---|---|
label | str | Property. Joint label, format {finger}_S{1..4}, e.g. "thumb_S1", "pinky_S4" |
index | int | Property. Global index (0–19) |
effort_limit() | Resource (SET/GET) | Single-joint torque cap (A) |
error_code() | Resource (GET) | The joint’s current error code — decode with describe_error(). Returns an integer, so format it as f"0x{code:04X}" if you print it directly |
status_word() | Resource (GET) | The joint’s status word |
enable() / disable() | Action | Enable / disable this joint |
clear_fault() | Action | Clear this joint’s faults |
set_origin() / clear_origin() | Action | Set / clear this joint’s user origin |
FingerHandle
Finger handle: traverse the 4 joints of this finger.
| Method | Return type | Description |
|---|---|---|
joints() | list[JointHandle] | The 4 joints of this finger, in S1..S4 order |
Joint Numbering
| Finger | S1 | S2 | S3 | S4 |
|---|---|---|---|---|
| thumb | 0 | 1 | 2 | 3 |
| index | 4 | 5 | 6 | 7 |
| middle | 8 | 9 | 10 | 11 |
| ring | 12 | 13 | 14 | 15 |
| pinky | 16 | 17 | 18 | 19 |
Label format: {finger}_S{1..4} (e.g. thumb_S1, pinky_S4). For the full table with motion ranges and model naming, see the Control Guide.
Data Types
JointStateFrame
Joint state frame: return value of hand.joint_states().subscribe().recv(). Variable-length, online joints only.
| Attribute | Type | Description |
|---|---|---|
header | FrameHeader | seq / timestamp_us / frame_id |
num_joints | int | Joint count in this frame |
joints | list[JointStateEntry] | Joint state array |
JointStateEntry
Single-joint state.
| Attribute | Type | Description |
|---|---|---|
nid | int | Node ID — identifies the joint across frames |
position | float | Position (rad, joint-side) |
velocity | float | Velocity (rad/s) |
effort | float | Torque (A) |
JointCommand
Joint command: element of the joint_command().publish().send() argument.
class JointCommand:
position: float # position (rad)
velocity: float # velocity (rad/s)
effort: float # torque feedforward (A) Python
C
Python: See the Python API examples earlier on this page.
The C API matches the Python API on this page and uses the wuji_hand_2_ prefix. For initialization conventions, memory management, error codes, and build setup, see the C API Reference.
Subscribe to Joint States
Variable-length frames carry a FrameHeader (seq + timestamp_us + frame_id = "l_wrist" or "r_wrist"). The joints array identifies online joints by nid:
void on_state(WujiFrameKind kind, const WujiJointStateFrame* frame, void* user) {
if (kind != WUJI_FRAME_KIND_OK) return;
printf("seq=%u frame=%s n=%u\n", frame->header.seq, frame->header.frame_id, frame->num_joints);
for (size_t i = 0; i < frame->joints_len; i++) {
const WujiJointStateEntry* j = &frame->joints[i];
printf(" nid=%u pos=%+.3f vel=%+.3f eff=%+.3f\n", j->nid, j->position, j->velocity, j->effort);
}
}
WujiSub* sub = NULL;
wuji_hand_2_subscribe_joint_states(dev, on_state, NULL, &sub);
// ... close with: wuji_sub_close(sub);nid is the firmware stream numbering, not the flat 0–19 joint index. Convert it with wuji_hand_2_nid_to_joint_index (see Stream NID and Joint Index below).
Stream NID and Joint Index
Both WujiJointStateEntry.nid and WujiJointDiagnosticsEntry.nid carry the firmware stream numbering, not the flat 0–19 joint index used by per-joint arrays and wuji_hand_2_joint_label. Two static functions convert between the two. Neither requires a device connection.
WujiStatus wuji_hand_2_nid_to_joint_index(uint8_t nid, uint8_t *out);
WujiStatus wuji_hand_2_joint_index_to_nid(uint8_t joint_index, uint8_t *out);Both return WujiStatus. *out is valid only when the call returns WUJI_STATUS_OK and is left untouched otherwise, so check the status before using the result. wuji_hand_2_nid_to_joint_index returns WUJI_STATUS_ERR_INVALID_ARG for a NULL out or a non-joint nid (ROOT nid 0 or any other non-joint ID). wuji_hand_2_joint_index_to_nid returns WUJI_STATUS_ERR_INVALID_ARG for a NULL out or an index outside 0–19. On failure, wuji_last_error() names the rejected value and lists the valid joint nids.
Realtime Command Publisher
Open the publisher, then call send with exactly 20 WujiJointCommand entries (each position / velocity / effort). Pass 0 for fields you do not want to feed forward:
WujiJointCommandPublisher* pub = NULL;
wuji_hand_2_joint_command_publish(dev, &pub);
WujiJointCommand cmds[20] = {0};
// cmds[i].position = ...; cmds[i].velocity = 0; cmds[i].effort = 0;
wuji_joint_command_publisher_send(pub, cmds);
wuji_joint_command_publisher_close(pub);Call send at a fixed rate (typically 200 Hz–1 kHz) to maintain continuous control. close is NULL-safe, but you still need to call wuji_joint_command_publisher_close to release the publisher handle, otherwise the resource leaks.
ドキュメント