C API Reference

Wuji SDK ships a C API (libwuji_sdk_c.so + wuji_sdk.h) with the same semantics as the Python interface — device discovery, connection, typed per-device subscription callbacks, global coordinate-transform streams, SDK user management, and Wuji Glove hand model calibration. For installation alongside Python, see Introduction.

The complete typed callback signatures and struct definitions live in include/wuji_sdk.h of the extracted tarball (download it from wuji-sdk Releases). The CMake example and build instructions are in the examples/c/ directory of the wuji-sdk repo.

Initialization and conventions

Initialization

Call once at process start and release at shutdown:

#include "wuji_sdk.h"

if (wuji_init(NULL) != WUJI_STATUS_OK) {
    fprintf(stderr, "init failed: %s\n", wuji_last_error());
    return 1;
}

// ... your code

wuji_shutdown();

Return value taxonomy

  • WujiStatus — most operations (wuji_init / wuji_scan / wuji_connect / opening subscriptions, etc.). WUJI_STATUS_OK means success. On failure, retrieve the per-thread error string via wuji_last_error().
  • int32_t — metadata readers (wuji_dev_serial_number / wuji_dev_device_name) return the number of bytes written.
  • uint8_t — status query wuji_dev_is_connected.
  • voidwuji_shutdown and resource cleanup functions.

Resource cleanup functions

  • wuji_discovered_free — release the device list returned by wuji_scan
  • wuji_dev_release — paired with wuji_connect, release the device handle after disconnecting
  • wuji_sub_close — close the subscription handle

Device discovery and connection

Scan for Wuji devices on the local network:

WujiDiscovered* devs = NULL;
size_t count = 0;
wuji_scan(&devs, &count);
for (size_t i = 0; i < count; i++) {
    printf("[%zu] %s (%s) @ %s\n", i, devs[i].serial_number, devs[i].model, devs[i].address);
}
wuji_discovered_free(devs, count);

Scan results carry the device type: device_id is a WujiDeviceType enum — compare it against the WUJI_DEVICE_TYPE_* constants (_WUJI_GLOVE / _WUJI_HAND_2 / _WUJI_HAND / _UNKNOWN) to tell what a device is before connecting. model carries the matching type string. _UNKNOWN means the type was not available at scan time or isn't recognized by this SDK build.

Connect by SN or address (target.kind is WUJI_CONNECT_TARGET_KIND_SN or _ADDR. The local alias is passed as the second argument to wuji_connect):

WujiConnectTarget target = {
    .kind = WUJI_CONNECT_TARGET_KIND_SN,
    .value = "WG1KA00260209001",
};
WujiConnectOptions opts = wuji_connect_options_default();
opts.timeout_ms = 5000;

WujiDevice* dev = NULL;
if (wuji_connect(&target, "glove", &opts, &dev) != WUJI_STATUS_OK) {
    fprintf(stderr, "connect failed: %s\n", wuji_last_error());
    return 1;
}

char sn_buf[64];
wuji_dev_serial_number(dev, sn_buf, sizeof(sn_buf));
printf("connected: sn=%s\n", sn_buf);

// ... your code

wuji_dev_disconnect(dev);
wuji_dev_release(dev);

device_name has the same semantics as in Python: a local alias that does not select a device or determine the returned handle type. It must be non-empty and must not contain / or .. See Device Connection.

For a WUJI_CONNECT_TARGET_KIND_ADDR target, set value to an endpoint that includes the port, such as 192.168.1.100:50000. A bare IP returns a missing-port error. Serial paths remain unsupported, so scan the device and connect by SN instead.

WujiConnectOptions fields match Python ConnectOptions: enable_bridge (default true) controls whether multiple SDK instances can connect to the same device, and auto_time_sync_interval_ms / auto_time_sync_interval_enabled configure background time sync (default 30000 ms — with it disabled, the first sync inside connect() still runs). We recommend starting from wuji_connect_options_default() and overriding the fields you need — zero-initializing the whole struct also zeroes these fields and changes connection behavior. Pass NULL to wuji_connect when you don't need custom options.

SDK User Management

The C SDK provides the same local user isolation as Python. A named SDK user keeps calibration artifacts and device parameters under ~/.wuji/sdk/users/<user_id>/. The default user is different: its device parameters stay in the shared ~/.wuji/sdk/params/, and it supports neither calibration nor a custom hand model. Switching users reloads the parameter stores of connected devices right away. For the isolation model and directory layout, see SDK Users and Calibration Data.

Create a user and switch to it:

WujiUserInfo created = {0};
wuji_create_user("Alice", "Right-hand operator", /*external_id=*/NULL, &created);

WujiUserInfo current = {0};
wuji_switch_user(created.user_id, &current);

wuji_user_info_free(&created);
wuji_user_info_free(&current);

List users, read the current user, and switch back to the default user:

WujiUserInfo* users = NULL;
size_t n = 0;
wuji_list_users(&users, &n);
for (size_t i = 0; i < n; i++) {
    printf("%s (%s)%s\n", users[i].display_name, users[i].user_id,
           users[i].is_default ? " [default]" : "");
}
wuji_user_info_array_free(users, n);

WujiUserInfo cur = {0};
wuji_current_user(&cur);
wuji_user_info_free(&cur);

WujiUserInfo def = {0};
wuji_switch_to_default_user(&def);
wuji_user_info_free(&def);

WujiUserInfo carries 7 fields: user_id, display_name, description, external_id, is_default, created_at, and updated_at. Update a user with wuji_update_user, passing only the fields you want to change through WujiUserUpdate. Set clear_description or clear_external_id to clear a value:

const char* user_id = "usr_xxxxxxxx";   // an existing user ID (from wuji_create_user / wuji_list_users)

WujiUserUpdate update = {
    .display_name = "Alice R.",
    .clear_description = true,   // clear the description
};
WujiUserInfo updated = {0};
wuji_update_user(user_id, &update, &updated);
wuji_user_info_free(&updated);

wuji_delete_user(user_id);

WujiUserInfo holds heap strings: free a single struct with wuji_user_info_free, and free an array returned by wuji_list_users with wuji_user_info_array_free. The default user doesn't support calibration or a custom hand model — create and switch to a named user first. See SDK Users and Calibration Data.

User Data Export and Import

Pack everything an SDK user owns — hand calibration models plus the tactile model and its latest complete calibration run — into one portable .zip for backup or migration. The three calls behave the same as their Python counterparts. For the semantics, see User Data Export and Import.

// Export: pass "" as user_id for the default user; the path must end in .zip
if (wuji_user_data_export("usr_xxxxxxxx", "alice.zip") != WUJI_STATUS_OK) {
    fprintf(stderr, "export failed: %s\n", wuji_last_error());
}

// Preview: run full validation without writing anything; OK means the bundle imports
if (wuji_user_data_preview("alice.zip") != WUJI_STATUS_OK) {
    fprintf(stderr, "bundle rejected: %s\n", wuji_last_error());
}

// Import: restore to the owner recorded in the bundle (created if absent); the current user stays
if (wuji_user_data_import("alice.zip") != WUJI_STATUS_OK) {
    fprintf(stderr, "import failed: %s\n", wuji_last_error());
}

All three return a WujiStatus — call wuji_last_error() for the reason on failure. Two status codes tell bundle problems apart:

  • WUJI_STATUS_ERR_NOT_FOUND — the bundle file doesn't exist
  • WUJI_STATUS_ERR_INVALID_DATA — the bundle is there but fails validation: a corrupt zip, a bad manifest, an sha256 mismatch, an unsafe entry path, or a size limit exceeded

Validation finishes before anything is written, so a failure leaves no file behind. The per-component report doesn't cross the FFI boundary. To inspect component-level results, use the Python or Rust API.

Subscribe to per-device data streams

Each typed data stream has a dedicated wrapper wuji_<device>_subscribe_<name>. The callback signature is bound to the stream schema. Wuji Glove tactile, for example:

void on_tactile(WujiFrameKind kind, const WujiTactileFrame* frame, void* user) {
    if (kind != WUJI_FRAME_KIND_OK) return;
    // frame->data, frame->header.seq, frame->header.timestamp_us
}

WujiSub* sub = NULL;
wuji_glove_subscribe_tactile(dev, on_tactile, /*user=*/NULL, &sub);

// ... callbacks fire from a background thread

wuji_sub_close(sub);

Wuji Glove typed subscribe entry points: tactile, tactile_zones, tactile_binary, tactile_residual, tactile_point_cloud, emf_poses, hand_joint_angles, hand_skeleton, tip_poses, imu_palm, and the per-finger imu_<finger> / imu_data_<finger> variants. Wuji Hand 2 exposes subscribe_joint_states, subscribe_joint_diagnostics, subscribe_imu, and the five subscribe_fingertip_<finger>_data variants. See Wuji Hand 2 Fingertip Tactile for the tactile format. The joint_states / joint_diagnostics frames are variable-length, contain only online joints, and are identified by nid per entry. Wuji Hand (first generation) exposes subscribe_joint_states (WujiHandJointStates, fixed-length 20 joints) plus the paired tactile glove's subscribe_tactile_pressure_frame / subscribe_tactile_status (WujiTactileGlove* types) — see Wuji Hand C API.

Stream output rate control

wuji_sub_set_rate adjusts the firmware push rate of a stream on its subscription handle. It returns a status code and writes the rate the device applies to out_actual_hz. It works on both the Wuji Glove and the Wuji Hand 2:

uint16_t actual_hz = 0;
if (wuji_sub_set_rate(sub, 30, &actual_hz) == WUJI_STATUS_OK)
    printf("device applied %u Hz\n", actual_hz);

// Pass 0 to restore the device default full rate
if (wuji_sub_set_rate(sub, 0, &actual_hz) == WUJI_STATUS_OK)
    printf("restored to %u Hz\n", actual_hz);
  • Call it on an open subscription handle.
  • Rates can only go down, not above the native rate. The device quantizes the request to a supported rate (an integer division of the native rate) and writes the applied value to out_actual_hz. Pass NULL if you don't need it.
  • The rate belongs to the stream, not to the handle: all subscribers of the stream share the setting, and the last write wins.
  • The setting doesn't persist: the rate resets to the device default full rate when the last subscription to the stream is closed or the device reconnects.
  • Only streams produced directly by the device support rate control. Derived streams (hand_joint_angles / hand_skeleton / tip_poses / imu_data_<finger>, and so on) follow their source rate automatically, and global resources such as tf_static are aggregated by the SDK. Calling on either returns WUJI_STATUS_ERR_UNSUPPORTED.
  • Requires firmware with stream rate control support. Firmware without it returns WUJI_STATUS_ERR_UNSUPPORTED (-8).

The rate-adjustable streams per device match Python — see Data Subscription — Adjusting the Stream Output Rate. Complete examples: examples/c/wuji_glove/1_set_stream_rate.c and examples/c/wuji_hand_2/4_set_stream_rate.c.

Wuji Hand 2 C API

Wuji Hand 2 mirrors the Python WujiHand2 resource-style interface on the C side. Control actions accept a 20-joint mask, whole-hand feedback arrives through subscription streams (variable-length, online joints only), and realtime commands are published through an AoS publisher.

Control actions (with 20-joint mask)

enable / disable / clear_fault / set_origin / clear_origin accept an optional const uint8_t mask[20]. Pass NULL to act on every joint. emergency_stop only acts on the whole hand and accepts no mask:

// Whole-hand enable
wuji_hand_2_enable(dev, NULL);
// Whole-hand clear faults
wuji_hand_2_clear_fault(dev, NULL);

// Index finger only (flat indices 4..7)
uint8_t mask[20] = {0};
for (int i = 4; i < 8; i++) mask[i] = 1;
wuji_hand_2_enable(dev, mask);            // enable only the 4 index-finger joints
wuji_hand_2_clear_fault(dev, mask);       // clear faults only on the 4 index-finger joints

// Whole-hand emergency stop
wuji_hand_2_emergency_stop(dev);

// User origin
wuji_hand_2_set_origin(dev, NULL);
wuji_hand_2_clear_origin(dev, NULL);

MIT params and effort limit (with online bitmap)

Whole-hand configuration writes support both a uniform value and per-joint arrays:

// Uniform effort_limit = 1.5 A
wuji_hand_2_set_all_effort_limit(dev, 1.5f);

// Per-joint write (flat-20, finger-major)
float limits[20] = { /* ... */ };
wuji_hand_2_set_all_effort_limit_per_joint(dev, limits);

float kp[20] = { /* ... */ }, kd[20] = { /* ... */ };
wuji_hand_2_set_all_mit_params(dev, kp, kd);

Reads return a flat-20 array plus an online bitmap. Offline joint slots are zero (or NaN for mit_params):

float read_back[20];
uint32_t online = 0;
wuji_hand_2_get_all_effort_limit(dev, read_back, &online);
for (int i = 0; i < 20; i++) {
    if (WUJI_JOINT_ONLINE(online, i)) {
        printf("joint %d: limit=%.2fA\n", i, read_back[i]);
    }
}

Writes reject NaN / infinity / negative values with WUJI_STATUS_ERR_INVALID_ARG. Filter invalid values before writing.

Subscribing to whole-hand feedback streams

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

wuji_hand_2_subscribe_joint_diagnostics has the same callback shape. Each WujiJointDiagnosticsEntry carries status_word, current, vbus_v_fb, mcu_temp_c_fb, error_code_current, and per-joint bus comm quality (comm_response_rate_pct, comm_timeout_total). The frame-level comm field (WujiHand2CommSummary) adds end-to-end stream loss, RPC retry/timeout counters, and the tactile online bitmap. wuji_hand_2_subscribe_imu fires with WujiImuData (same schema as Wuji Glove imu_*).

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, a tactile slot, or an out-of-range value). 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.

Decode Joint Status Words

wuji_hand_2_decode_joint_status decodes WujiJointDiagnosticsEntry.status_word into WujiJointStatus. ext_state is 0 for initializing, 1 for ready, 2 for enabled, or 3 for stopped. enabled is true only when ext_state == 2. The remaining fields show whether the position, velocity, and current limits are active. The function doesn't require a device connection.

void print_joint_status(uint32_t status_word) {
    WujiJointStatus status = {0};
    if (wuji_hand_2_decode_joint_status(status_word, &status) != WUJI_STATUS_OK) {
        return;
    }

    printf("state=%u enabled=%d limits=%d/%d/%d\n",
           status.ext_state,
           status.enabled,
           status.position_limit_active,
           status.velocity_limit_active,
           status.current_limit_active);
}

The function returns WUJI_STATUS_ERR_INVALID_ARG when out is NULL.

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.

Error code description

wuji_hand_2_describe_error is a static function (no device connection needed) that decodes a device fault code (from joint_diagnostics's error_code_current — the same values carried by the error history and calibration results in the Python SDK) into WujiErrorInfo. It accepts device fault codes only. Calibration in the Python SDK also reports a separate encoder-only error byte — that one is a different, unrelated enum and must not be passed in. The WujiHandJointDiagnostics.error_code bitfield from Wuji Hand joint diagnostics (returned by wuji_hand_get_all_diagnostics) must not be passed in either:

WujiErrorInfo info = {0};
// 0x2102 = Overcurrent (ImmediateStop / ManualClear), a real production fault code
if (wuji_hand_2_describe_error(0x2102, &info) == WUJI_STATUS_OK) {
    printf("name=%s severity=%s\ndesc=%s\ncause=%s\n",
           info.name, info.severity, info.desc, info.cause);
}

Unknown codes return WUJI_STATUS_ERR_NOT_FOUND. severity values: Warning / DeferredStop / ImmediateStop / Fatal. clear_policy values: AutoClear / ManualClear / NonClearable.

Identity and diagnostics

WujiHandedness side;
wuji_hand_2_get_handedness(dev, &side);   // WUJI_HANDEDNESS_LEFT / _RIGHT

uint8_t n_online;
wuji_hand_2_online_joints_count(dev, &n_online);  // 0..20

// Two-call string getter: first probe with buf=NULL, buf_len=0 to learn the required size
size_t needed = 0;
wuji_hand_2_get_ip(dev, NULL, 0, &needed);
char* ip = malloc(needed);
wuji_hand_2_get_ip(dev, ip, needed, NULL);
printf("ip=%s\n", ip);
free(ip);

get_hw_version / get_comm_diag return an out struct with heap-owned fields. Release with the matching _free function after use (only when the call returned WUJI_STATUS_OK).

Data Port

wuji_hand_2_get_port reads the data port (the UDP port the device accepts SDK connections on, 50001 by factory default), and wuji_hand_2_set_port saves a new port. Changing the port requires recent Wuji Hand 2 firmware and Wuji SDK. Upgrade both to the latest version first.

uint16_t port = 0;
wuji_hand_2_get_port(dev, &port);          // e.g. 50001

if (wuji_hand_2_set_port(dev, 50002) != WUJI_STATUS_OK) {
    fprintf(stderr, "set port failed: %s\n", wuji_last_error());
} else {
    wuji_hand_2_reboot(dev);               // switch to the new port after reboot
}

The new port takes effect after the device reboots and persists across power loss. Once wuji_hand_2_set_port succeeds, wuji_hand_2_get_port returns the new port right away. Valid ports are 10000–65535, except the discovery port 50000. An out-of-range port makes wuji_hand_2_set_port return a status other than WUJI_STATUS_OK, with Invalid argument in wuji_last_error(), and the port keeps its current value. Firmware earlier than v2.10.0 doesn't accept the write and also returns an error status. After the reboot, connections by address need the new port. Connections through device discovery need no change.

Flash Log Export

C SDK v2026.8.31 and later provides the Wuji Hand 2 Flash log export API. After calling wuji_hand_2_export_flash_logs(), the current thread waits until the SDK reads the device logs and writes the output file before the function returns a result struct with path and frame_count. The output file uses JSONL format, with one JSON record per line. Wuji Hand 2 firmware v2.6.0 and later supports concurrent exports from the same device. Export logs one at a time with earlier firmware. Firmware or protocol versions that don't support Flash logs report the export as unsupported:

WujiFlashLogExport result = {0};
if (wuji_hand_2_export_flash_logs(dev, NULL, &result) == WUJI_STATUS_OK) {
    printf("%u frames -> %s\n", result.frame_count, result.path);
    wuji_hand_2_flash_log_export_free(&result);
} else {
    fprintf(stderr, "export failed: %s\n", wuji_last_error());
}

Pass NULL as out_dir to write flash_<serial>_<date>_<time>.jsonl under ~/.wuji/logs/, or pass a directory path to choose the destination. Each call creates a new file. Exports created within the same second receive a numbered suffix instead of replacing an existing file.

The returned frame count can be 0 when the device log ring is empty. The empty output file is still a successful export.

Handle common export errors as follows:

  • C returns WUJI_STATUS_ERR_INVALID_ARG when dev or out is NULL, or when out_dir isn't valid UTF-8.
  • C returns WUJI_STATUS_ERR_UNSUPPORTED when the device isn't Wuji Hand 2 or its firmware doesn't support the current protocol.
  • C returns WUJI_STATUS_ERR_TIMEOUT if another export is running or if log data is overwritten during export and repeated retries still can't complete. Check wuji_last_error() for details.
  • C returns WUJI_STATUS_ERR_DISCONNECTED if the device disconnects during export.
  • C returns WUJI_STATUS_ERR_PROTOCOL when the device reports a flash-log protocol error or returns malformed data.
  • C returns WUJI_STATUS_ERR_INTERNAL when a file-system I/O operation fails. Check wuji_last_error() for details.

For other status values, call wuji_last_error() to get the cause. A failed export doesn't create an output file.

Call wuji_hand_2_flash_log_export_free only after a successful export.

Wuji Hand C API

Wuji Hand (USB) is also available through the C SDK. The API shape matches Wuji Hand 2 — command structs, subscription callbacks, and function naming are aligned, so control code is portable between the two hands.

The Wuji Hand C API is available only in the Linux x86_64 / aarch64 (gnu) tarballs, which ship a single library file, libwuji_sdk_c.solibstdc++.so.6 and libusb-1.0.so.0 are no longer runtime dependencies. On the Android tarball these functions are present but return WUJI_STATUS_ERR_UNSUPPORTED.

If you deployed libwujihandcpp.so from an earlier tarball, you can delete it — the SDK doesn't load it anymore. The C API and link line are unchanged, so existing applications keep working without a rebuild.

Connection and whole-hand control

wuji_hand_connect_sn connects by USB serial number (equivalent to wuji_connect with kind SN). Control actions act on the whole hand and take no 20-joint mask:

// Scan first to discover devices and their USB serial numbers
WujiDiscovered* devs = NULL;
size_t count = 0;
wuji_scan(&devs, &count);

// Select the target Wuji Hand by device type (device_id == WUJI_DEVICE_TYPE_WUJI_HAND), then free the list after connecting
WujiDevice* dev = NULL;
for (size_t i = 0; i < count; i++) {
    if (devs[i].device_id == WUJI_DEVICE_TYPE_WUJI_HAND) {
        if (wuji_hand_connect_sn(devs[i].serial_number, "wuji_hand", &dev) != WUJI_STATUS_OK) {
            fprintf(stderr, "connect failed: %s\n", wuji_last_error());
            dev = NULL;
        }
        break;
    }
}
wuji_discovered_free(devs, count);

if (dev == NULL) {                            // No first-gen Hand found, or the connect failed
    return 1;
}

wuji_hand_enable(dev);                        // enable all 20 joints
wuji_hand_set_all_effort_limit(dev, 1.5f);    // effort limit (amps)

float pos[20];
wuji_hand_read_joint_state(dev, pos);         // 20-joint position snapshot

wuji_hand_disable(dev);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);

wuji_hand_clear_all_faults clears faults on the whole hand. Diagnostics and metadata readers: wuji_hand_get_all_diagnostics (bus voltage, temperature, error code), wuji_hand_get_soft_limits (firmware soft-limit bounds), wuji_hand_get_handedness, and wuji_hand_get_firmware_version / get_product_sn / get_input_voltage / get_temperature. wuji_hand_joint_label / wuji_hand_finger_name return joint and finger names (static functions, no connection needed).

Both handedness getters return a raw uint8_t, but their encodings are opposites—don't mix them. wuji_hand_get_handedness returns 0 for right and 1 for left, the reverse of the WujiHandedness enum, so reading it through the enum swaps left and right. wuji_hand_get_tactile_handedness returns 0 for left and 1 for right, which matches the enum.

Realtime commands and controller

The publisher has the same shape as Wuji Hand 2: send reads exactly 20 WujiJointCommand entries. The low-pass-filtered realtime position controller reads back actual position and actual effort:

// AoS publisher
WujiHandJointCommandPublisher* pub = NULL;
wuji_hand_joint_command_publish(dev, &pub);
WujiJointCommand cmds[20] = {0};
wuji_hand_joint_command_publisher_send(pub, cmds);
wuji_hand_joint_command_publisher_close(pub);

// Low-pass-filtered realtime position controller
WujiRealtimeController* ctrl = NULL;
WujiLowPass filter = { .cutoff_hz = 5.0 };
wuji_hand_realtime_controller_open(dev, filter, &ctrl);

float target[20] = {0};
wuji_hand_realtime_controller_set_target_position(ctrl, target);

float actual_pos[20], actual_eff[20];
wuji_hand_realtime_controller_get_actual_position(ctrl, actual_pos);
wuji_hand_realtime_controller_get_actual_effort(ctrl, actual_eff);   // actual effort (amps)

wuji_hand_realtime_controller_close(ctrl);

Both get_actual_* calls read from a non-blocking cache, so control frequency is unaffected.

Paired tactile glove

wuji_hand_is_tactile_attached probes whether a tactile glove is attached. Data subscriptions: wuji_hand_subscribe_tactile_pressure_frame (WujiTactileGloveFrame, 20×31 pressure grid) and wuji_hand_subscribe_tactile_status (WujiTactileGloveStatus). Info readers: wuji_hand_get_tactile_device_info / get_tactile_diagnostics / get_tactile_handedness.

When the glove is missing or faulty, these calls return an error status — read the details with wuji_last_error().

Wuji Glove resource API

Wuji Glove exposes set / get / exec resource APIs aligned with Python.

Device parameter read / write

Identity and network parameters:

// String typed getters use a two-call query: first probe with buf=NULL, then allocate and fill
size_t needed = 0;
wuji_glove_get_sn(dev, NULL, 0, &needed);
char* sn = malloc(needed);
wuji_glove_get_sn(dev, sn, needed, NULL);

wuji_glove_get_version(dev, NULL, 0, &needed);
char* fw = malloc(needed);
wuji_glove_get_version(dev, fw, needed, NULL);

wuji_glove_get_ip(dev, NULL, 0, &needed);
char* ip = malloc(needed);
wuji_glove_get_ip(dev, ip, needed, NULL);

wuji_glove_get_hand_side(dev, NULL, 0, &needed);
char* side = malloc(needed);   // "left" / "right"
wuji_glove_get_hand_side(dev, side, needed, NULL);

uint16_t port = 0;
wuji_glove_get_port(dev, &port);

// Write IP and port (requires reconnecting after the change)
wuji_glove_set_ip(dev, "192.168.1.100");
wuji_glove_set_port(dev, 50001);

free(sn); free(fw); free(ip); free(side);

Device control

// Reboot the device. The device disconnects, so wuji_connect again afterwards.
wuji_glove_reboot(dev);

Time synchronization

wuji_glove_sync_time triggers one full round-trip sync, mirroring Python glove.sync_time(). The result lands in WujiTimeSyncResult (offset_us, round_trip_us, and synced_at_us, all in microseconds). The struct is plain POD — no heap fields, no _free needed. For the full semantics, see Time Synchronization and Coordinate Transforms.

WujiTimeSyncResult tsr = {0};
if (wuji_glove_sync_time(dev, &tsr) == WUJI_STATUS_OK) {
    printf("offset=%lldus rtt=%lldus synced_at=%lluus\n",
           (long long)tsr.offset_us, (long long)tsr.round_trip_us,
           (unsigned long long)tsr.synced_at_us);
}

The call blocks until the round-trip completes, serializes with the SDK's periodic background sync task, and leaves *out untouched on failure. Configure the background sync interval and switch through the WujiConnectOptions fields auto_time_sync_interval_ms / auto_time_sync_interval_enabled.

Tactile Contact Sensitivity

wuji_glove_set_tactile_binary_sensitivity sets the tactile_binary sensitivity multiplier for the active SDK user. The SDK stores the value locally. It takes effect immediately and persists across reconnects. Pass a finite positive value. Values above 1.0 make contact detection more sensitive.

wuji_glove_get_tactile_binary_sensitivity returns the multiplier currently in effect. The returned value is limited to 0.5–3.0. If no valid value is available, the getter returns 1.0.

double sensitivity = 0.0;
if (wuji_glove_get_tactile_binary_sensitivity(dev, &sensitivity) == WUJI_STATUS_OK) {
    printf("effective sensitivity: %.2f\n", sensitivity);
}

Both dev and out_sensitivity must be non-NULL. If either pointer is NULL, the getter returns WUJI_STATUS_ERR_INVALID_ARG. When out_sensitivity is valid, the getter leaves *out_sensitivity unchanged on failure. Call wuji_last_error() for details.

Custom hand model for online IK

wuji_glove_set_hand_model_path sets a custom hand URDF path and wuji_glove_get_hand_model_path reads it back, mirroring Python glove.hand_model_path().set(path) / .get(). Setting a path reloads the online-IK streams (hand_joint_angles, tip_poses, hand_skeleton) against the custom URDF through the SDK's calibration-generation mechanism. The getter uses the two-call size query — pass NULL to get the required length, then call again to fill:

// Set a custom hand URDF for online IK
wuji_glove_set_hand_model_path(dev, "/path/to/hand.urdf");

// Read it back (two-call size query)
size_t needed = 0;
wuji_glove_get_hand_model_path(dev, NULL, 0, &needed);
char *path = malloc(needed);
wuji_glove_get_hand_model_path(dev, path, needed, NULL);
printf("hand model: %s\n", path);
free(path);

Only a named SDK user can set a custom hand model: online IK under the default SDK user always uses the built-in default URDF, so calling wuji_glove_set_hand_model_path there fails and wuji_last_error tells you to switch to a named user first. An unreadable path also fails immediately and is never stored.

Hand Model Calibration

Wuji Glove hand model calibration offers both synchronous and asynchronous entry points in C, matching Python glove.calibrate(). Calibration is per-user, so switch to a named SDK user first — the default user fails calibration outright. For the calibration actions, pose sequence, and artifacts, see Wuji Glove Calibration.

Get default options with wuji_glove_calibration_options_default (skip_constraints to skip constraint checks, timeout_s for the timeout in seconds). The feedback callback receives a WujiGloveCalibrationFeedback with the current pose progress. Only state and progress are always present. Every other scalar field is guarded by a companion has_* flag — read step_index only when has_step_index is true, and the same for step_total, step_name, hold_elapsed, frames_collected, and the rest. metrics and hints are arrays with no has_* flag: check metrics_len / hints_len instead. Each WujiGloveCalibrationMetric carries its own guards: a metric has finger / finger_b only when has_finger / has_finger_b is true, so metrics aren't necessarily per-finger.

For an async session, start begins calibration, then poll with try_finish or block with wait until it finishes, cancel requests cooperative cancellation, and session_free releases the handle:

void on_feedback(const WujiGloveCalibrationFeedback* fb, void* user) {
    // fb->state / fb->progress / fb->step_index / fb->metrics ...
    // fb is valid only during the callback; copy any fields you need to keep
}

WujiGloveCalibrationOptions options;
wuji_glove_calibration_options_default(&options);

WujiGloveCalibrationSession* session = NULL;
WujiStatus st = wuji_glove_calibration_start(dev, &options, on_feedback, /*user_data=*/NULL, &session);

WujiGloveCalibrationResult result = {0};
bool done = false;
while (st == WUJI_STATUS_OK && !done) {
    if (/* Ctrl+C received */) wuji_glove_calibration_cancel(session);
    st = wuji_glove_calibration_try_finish(session, &done, &result);
    if (st != WUJI_STATUS_OK || done) break;
    nanosleep(&(struct timespec){ .tv_nsec = 50 * 1000 * 1000 }, NULL);   // poll interval ~50 ms
}
if (session) wuji_glove_calibration_session_free(session);

// Read the result only when start succeeded and calibration finished cleanly; don't touch result fields on cancel or failure
if (st == WUJI_STATUS_OK && done) {
    printf("side=%s poses=%u urdf=%s user=%s\n",
           handedness_name(result.handedness), result.poses_collected,
           result.calibrated_urdf, result.sdk_user.display_name);
    wuji_glove_calibration_result_free(&result);
}

The synchronous entry point wuji_glove_calibrate blocks in a single call until calibration finishes, takes the same parameters, and writes straight into a WujiGloveCalibrationResult:

WujiGloveCalibrationResult result = {0};
wuji_glove_calibrate(dev, &options, on_feedback, /*user_data=*/NULL, &result);
wuji_glove_calibration_result_free(&result);

WujiGloveCalibrationResult carries the pose count poses_collected, per-pose frame counts frames_per_pose, handedness handedness, the generated URDF path calibrated_urdf, and the owning user snapshot sdk_user. Treat calibrated_urdf as an opaque SDK-allocated local path and don't depend on its parent-directory layout. Free the result with wuji_glove_calibration_result_free when you're done.

Cancellation is cooperative. wuji_glove_calibration_cancel returning WUJI_STATUS_OK only means the request was recorded — it doesn't mean calibration stopped. The run maps to WUJI_STATUS_ERR_CANCELLED only when the cancellation lands before results are published. Once the run reaches the publishing stage, it can still finish successfully or fail with a publishing error, so always check the status the session actually returns rather than assuming a cancelled run yields ERR_CANCELLED. A device that doesn't support calibration maps to WUJI_STATUS_ERR_UNSUPPORTED.

Glove tactile contact calibration

wuji_glove_calibrate_tactile_blocking runs the guided tactile contact calibration in one call: you record a few prompted hand motions, then the SDK collects the data, validates it, and trains the model. See Wuji Glove Tactile Data for the streams.

Decide install before you call:

  • install = true: after training, the SDK runs load validation and installs the model. It's stored per SDK user and glove serial number, and loads automatically when you subscribe to tactile_binary / tactile_residual. The optional sensitivity pair (has_sensitivity set to true plus sensitivity) is only accepted in this mode — otherwise the call fails.
  • install = false: the SDK trains only. Load validation and install are skipped, the artifacts stay in the run directory, and nothing loads automatically. Use this to evaluate a training run offline before deciding whether to install.

Make the call:

// uint32_t on_feedback(const WujiTactileCalibrationFeedback* event, void* user);
// uint32_t on_pose_prompt(const WujiTactilePromptRequest* request, void* user);
WujiTactileCalibrationOptions options = {
    .seconds_per_pose = 10.0f,
    .epochs = 60,
    .install = true,
    .timeout_s = 1800.0,
};
WujiTactileCalibrationCallbacks callbacks = {
    .on_feedback = on_feedback,        // collect and train progress
    .on_pose_prompt = on_pose_prompt,  // before and after each motion: proceed, retry, or stop
};
WujiTactileCalibrationSummary summary = {0};
if (wuji_glove_calibrate_tactile_blocking(dev, &options, &callbacks, &summary) == WUJI_STATUS_OK) {
    // read the summary, then release its heap fields
    wuji_tactile_calibration_summary_free(&summary);
}

on_pose_prompt makes the flow interactive: it fires before each motion starts and after each recording ends with a WujiTactilePromptRequest, and returns 0 to proceed, 1 to retry, or 2 to stop — any other value stops the run. on_feedback reports collect and train progress, and returns 0 to continue or 1 to abort — any other value aborts. Leave both unset for a hands-off run. Event pointers passed to callbacks are only valid for the duration of the callback. timeout_s is cooperative: on timeout the calibration is cancelled, but the function first waits for in-flight collect or train work and any running callback to return, so no callback ever fires after the function returns. If on_pose_prompt blocks indefinitely (for example, waiting on console input), the timeout can't force it to return.

On success, read the results from summary. model_dir is populated in both outcomes — use installed to tell them apart:

  • installed = true: model_dir points to the installed model, which loads automatically when you subscribe.
  • installed = false: model_dir points to the model saved in this run, which is not loaded automatically.

verified_alive_taxels is only valid when has_verified_alive_taxels is true. Call wuji_tactile_calibration_summary_free to release the heap fields once you're done.

Retrieve failure details with wuji_last_error(). A library built without the model-export feature returns WUJI_STATUS_ERR_UNSUPPORTED. The Python counterpart is glove.calibrate_tactile_blocking() — the async glove.calibrate_tactile() is Python-only, with no async C entry point.

Subscribe to global resources

The cross-device aggregated coordinate transforms exposed in Python as manager.tf() / manager.tf_static() map to device-less C wrappers:

void on_tf(WujiFrameKind kind, const WujiFrameTransforms* frames, void* user) {
    if (kind != WUJI_FRAME_KIND_OK) return;
    for (size_t i = 0; i < frames->transforms_len; i++) {
        const WujiFrameTransform* t = &frames->transforms[i];
        printf("%s -> %s\n", t->parent_frame_id, t->child_frame_id);
    }
}

WujiSub* sub = NULL;
wuji_subscribe_tf(on_tf, NULL, &sub);
// Same shape: wuji_subscribe_tf_static(...)

wuji_sub_close(sub);

Retargeting

Maps 21 human hand keypoints (MediaPipe order) to 20 joint angles in firmware order, matching the Python RetargetSession. For concepts and input format, see Hand Retargeting.

WujiRetargetSession* session = NULL;
WujiStatus st = wuji_retarget_session_create(
    WUJI_HAND_MODEL_WUJI_HAND2, WUJI_HANDEDNESS_RIGHT, &session);
if (st != WUJI_STATUS_OK) {
    fprintf(stderr, "create failed: %s\n", wuji_last_error());
    return 1;
}

float keypoints[63];   // 21×3, row-major xyz, in meters, MediaPipe landmark order
float qpos[20];        // joint angles (rad) in firmware order
/* fill keypoints from your source */
st = wuji_retarget_session_step(session, keypoints, qpos);
if (st != WUJI_STATUS_OK) {
    // Degenerate / invalid keypoint frames return WUJI_STATUS_ERR_ALGORITHM — drop the frame
    fprintf(stderr, "step failed: %s\n", wuji_last_error());
}
// After switching data sources or a tracking gap, reset warm-start and filter state:
wuji_retarget_session_reset(session);

wuji_retarget_session_free(session);   // NULL is a no-op
  • model takes WUJI_HAND_MODEL_WUJI_HAND / WUJI_HAND_MODEL_WUJI_HAND2, and side takes WUJI_HANDEDNESS_LEFT / WUJI_HANDEDNESS_RIGHT
  • Degenerate or invalid keypoint frames return WUJI_STATUS_ERR_ALGORITHM — get details via wuji_last_error()
  • For complete examples (including a Wuji Glove input → retarget → drive-the-device teleop loop), see examples/c/retargeting/

Usage constraints

C SDK users must observe the following constraints to avoid use-after-free, deadlocks, and reading stale error strings.

  • A typed callback's frame pointer is valid only for the duration of the callback. Heap fields are released after the callback returns. Copy whatever you need to retain inside the callback. Do not store the pointer.
  • Never call wuji_sub_close from inside that subscription's own callback. close joins the worker thread and will deadlock if called from the callback itself. Close from a different thread.
  • The return value of wuji_last_error() is only valid until the next wuji_* call on the same thread. Copy the message immediately if you need to retain it.
  • END and ERROR are terminal frames — no further data will arrive after them, but you still need to call wuji_sub_close to release the subscription handle.
  • String getters use a two-call query: first call with buf=NULL, buf_len=0 to read *needed (required bytes including NUL), then allocate and call again to fill. Passing an undersized buffer returns WUJI_STATUS_ERR_BUFFER_TOO_SMALL and never truncates.
  • Whole-hand batch reads (get_all_effort_limit / get_all_mit_params) return a flat-20 array plus an online bitmap. Use the WUJI_JOINT_ONLINE(mask, i) macro to check slot validity, otherwise you will read offline-joint placeholders (0 or NaN).
  • The calibration feedback callback runs on the calibration worker thread, and the feedback pointer is valid only during the callback. The SDK guards the session calls made from inside it rather than deadlocking: wuji_glove_calibration_wait and wuji_glove_calibration_try_finish return WUJI_STATUS_ERR_INVALID_ARG, and wuji_glove_calibration_session_free records the reason in wuji_last_error() and skips the release — so the session stays alive and you must free it again after the callback returns. wuji_glove_calibration_cancel is safe to call from the callback. Free the session with wuji_glove_calibration_session_free and the result with wuji_glove_calibration_result_free.
  • WujiUserInfo holds heap strings: free a single struct with wuji_user_info_free, and free an array returned by wuji_list_users with wuji_user_info_array_free.

Python equivalents

CapabilityCPython
Initwuji_init(NULL)SdkManager.instance()
Scanwuji_scan(&devs, &count)manager.scan()
Connectwuji_connect(&target, alias, &opts, &dev)manager.connect(...) / manager.auto_connect(...)
Default connect optionswuji_connect_options_default()ConnectOptions()
Device subscribewuji_glove_subscribe_tactile(dev, cb, user, &sub)glove.tactile().subscribe_with_callback(...)
Global subscribewuji_subscribe_tf(cb, user, &sub)manager.tf().subscribe()
Stream output ratewuji_sub_set_rate(sub, hz, &actual)sub.set_rate(hz)
Hand 2 control actionwuji_hand_2_enable(dev, mask)hand.enable(joints=mask)
Hand 2 realtime commandwuji_hand_2_joint_command_publish + wuji_joint_command_publisher_sendhand.joint_command().publish().send([...])
Hand 2 joint state subscribewuji_hand_2_subscribe_joint_stateshand.joint_states().subscribe()
Hand 2 joint diagnostics subscribewuji_hand_2_subscribe_joint_diagnosticshand.joint_diagnostics().subscribe()
Hand 2 IMU subscribewuji_hand_2_subscribe_imuhand.imu().subscribe()
Hand 2 nid and joint index conversionwuji_hand_2_nid_to_joint_index(nid, &index) / wuji_hand_2_joint_index_to_nid(joint_index, &nid)WujiHand2.nid_to_joint_index(nid) / WujiHand2.joint_index_to_nid(joint_index)
Hand 2 error descriptionwuji_hand_2_describe_error(code, &info)WujiHand2.describe_error(code)
Hand 2 data portwuji_hand_2_get_port(dev, &port) / wuji_hand_2_set_port(dev, port)hand.port().get() / hand.port().set(port)
Hand 2 flash log exportwuji_hand_2_export_flash_logs + wuji_hand_2_flash_log_export_freehand.export_flash_logs()
Hand connect (USB SN)wuji_hand_connect_sn(sn, alias, &dev)WujiHand.connect_sn(sn, alias)
Hand joint state subscribewuji_hand_subscribe_joint_stateshand.joint_states().subscribe()
Hand realtime commandwuji_hand_joint_command_publish + wuji_hand_joint_command_publisher_sendhand.joint_command().publish().send([...])
Hand realtime controllerwuji_hand_realtime_controller_open / _set_target_position / get_actual*hand.realtime_controller(LowPass(...))
Glove time syncwuji_glove_sync_time(dev, &tsr)glove.sync_time()
Glove hand model pathwuji_glove_get_hand_model_path / wuji_glove_set_hand_model_pathglove.hand_model_path().get() / .set(path)
SDK user managementwuji_create_user / wuji_switch_user / wuji_list_usersmanager.create_user() / switch_user() / list_users()
User data export and importwuji_user_data_export / wuji_user_data_preview / wuji_user_data_importmanager.export_user_data() / preview_user_data() / import_user_data()
Glove hand model calibration (async)wuji_glove_calibration_start + wuji_glove_calibration_try_finishglove.calibrate()
Glove hand model calibration (sync)wuji_glove_calibrateglove.calibrate_blocking()
Glove tactile contact calibration (sync)wuji_glove_calibrate_tactile_blockingglove.calibrate_tactile_blocking()
Disconnectwuji_dev_disconnect + wuji_dev_releasedevice.disconnect()

CMake project example

A complete runnable C project (with CMakeLists.txt plus a subscription callback demo) lives in the examples/c/ directory of the wuji-sdk repo, organized into examples/c/wuji_glove/ (stream output rate control, custom hand model for online IK, SDK user management, and hand model calibration), examples/c/wuji_hand_2/ (joint-state subscriptions, control actions, device info, fingertip tactile data, stream output rate control, flash-log export, opposition replay, and MIT sweep), and examples/c/wuji_hand/ (subscriptions, publishing, grasp loops, and tactile status) subdirectories. See that directory's README for build instructions. Minimal link:

cc my_app.c -I "${SDK}/include" -L "${SDK}/lib" -lwuji_sdk_c -o my_app
LD_LIBRARY_PATH="${SDK}/lib" ./my_app

${SDK} is the extracted tarball root. The CMake project uses -DWUJI_SDK_INCLUDE_DIR and -DWUJI_SDK_LIB to point at the same paths.

Subscribe to Updates