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 IK 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_OKmeans success. On failure, retrieve the per-thread error string viawuji_last_error().int32_t— metadata readers (wuji_dev_serial_number/wuji_dev_device_name) return the number of bytes written.uint8_t— status querywuji_dev_is_connected.void—wuji_shutdownand resource cleanup functions.
Resource cleanup functions
wuji_discovered_free— release the device list returned bywuji_scanwuji_dev_release— paired withwuji_connect, release the device handle after disconnectingwuji_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.
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 Device Parameters and User Isolation.
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, ¤t);
wuji_user_info_free(&created);
wuji_user_info_free(¤t);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 Device Parameters and User Isolation.
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 existWUJI_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, and subscribe_imu. 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.
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);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_*).
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 an error code (from joint_diagnostics's error_code_current) into WujiErrorInfo:
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).
Wuji Hand C API
Wuji Hand (first generation, 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.so — libstdc++.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 first-generation 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);EMF output rate control
// Read the current divider (default 1 — no downsampling)
uint32_t divider = 0;
wuji_glove_get_emf_poses_rate_divider(dev, ÷r);
// Set N=4 → EMF poses output rate drops to input_rate/4 (120 Hz → ~30 Hz)
wuji_glove_set_emf_poses_rate_divider(dev, 4);EMF-pose-derived streams (hand_joint_angles / tip_poses / hand_skeleton / tactile_point_cloud) follow the same rate automatically. IMU and raw tactile streams are unaffected. For the full reference, see Wuji Glove SDK data reference.
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.
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.
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.
IK Calibration
Wuji Glove IK 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 totactile_binary/tactile_residual. The optional sensitivity pair (has_sensitivityset to true plussensitivity) 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_dirpoints to the installed model, which loads automatically when you subscribe.installed = false:model_dirpoints 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-opmodeltakesWUJI_HAND_MODEL_WUJI_HAND/WUJI_HAND_MODEL_WUJI_HAND2, andsidetakesWUJI_HANDEDNESS_LEFT/WUJI_HANDEDNESS_RIGHT- Degenerate or invalid keypoint frames return
WUJI_STATUS_ERR_ALGORITHM— get details viawuji_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
framepointer 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_closefrom inside that subscription's own callback.closejoins 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 nextwuji_*call on the same thread. Copy the message immediately if you need to retain it. ENDandERRORare terminal frames — no further data will arrive after them, but you still need to callwuji_sub_closeto release the subscription handle.- String getters use a two-call query: first call with
buf=NULL, buf_len=0to read*needed(required bytes including NUL), then allocate and call again to fill. Passing an undersized buffer returnsWUJI_STATUS_ERR_BUFFER_TOO_SMALLand never truncates. - Whole-hand batch reads (
get_all_effort_limit/get_all_mit_params) return a flat-20 array plus an online bitmap. Use theWUJI_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
feedbackpointer is valid only during the callback. The SDK guards the session calls made from inside it rather than deadlocking:wuji_glove_calibration_waitandwuji_glove_calibration_try_finishreturnWUJI_STATUS_ERR_INVALID_ARG, andwuji_glove_calibration_session_freerecords the reason inwuji_last_error()and skips the release — so the session stays alive and you must free it again after the callback returns.wuji_glove_calibration_cancelis safe to call from the callback. Free the session withwuji_glove_calibration_session_freeand the result withwuji_glove_calibration_result_free. WujiUserInfoholds heap strings: free a single struct withwuji_user_info_free, and free an array returned bywuji_list_userswithwuji_user_info_array_free.
Python equivalents
| Capability | C | Python |
|---|---|---|
| Init | wuji_init(NULL) | SdkManager.instance() |
| Scan | wuji_scan(&devs, &count) | manager.scan() |
| Connect | wuji_connect(&target, alias, &opts, &dev) | manager.connect(...) / manager.auto_connect(...) |
| Default connect options | wuji_connect_options_default() | ConnectOptions() |
| Device subscribe | wuji_glove_subscribe_tactile(dev, cb, user, &sub) | glove.tactile().subscribe_with_callback(...) |
| Global subscribe | wuji_subscribe_tf(cb, user, &sub) | manager.tf().subscribe() |
| Hand 2 control action | wuji_hand_2_enable(dev, mask) | hand.enable(joints=mask) |
| Hand 2 realtime command | wuji_hand_2_joint_command_publish + wuji_joint_command_publisher_send | hand.joint_command().publish().send([...]) |
| Hand 2 joint state subscribe | wuji_hand_2_subscribe_joint_states | hand.joint_states().subscribe() |
| Hand 2 joint diagnostics subscribe | wuji_hand_2_subscribe_joint_diagnostics | hand.joint_diagnostics().subscribe() |
| Hand 2 IMU subscribe | wuji_hand_2_subscribe_imu | hand.imu().subscribe() |
| Hand 2 error description | wuji_hand_2_describe_error(code, &info) | WujiHand2.describe_error(code) |
| Hand connect (USB SN) | wuji_hand_connect_sn(sn, alias, &dev) | WujiHand.connect_sn(sn, alias) |
| Hand joint state subscribe | wuji_hand_subscribe_joint_states | hand.joint_states().subscribe() |
| Hand realtime command | wuji_hand_joint_command_publish + wuji_hand_joint_command_publisher_send | hand.joint_command().publish().send([...]) |
| Hand realtime controller | wuji_hand_realtime_controller_open / _set_target_position / get_actual* | hand.realtime_controller(LowPass(...)) |
| Glove EMF rate divider | wuji_glove_set_emf_poses_rate_divider(dev, N) | glove.emf_poses_rate_divider().set(N) |
| Glove time sync | wuji_glove_sync_time(dev, &tsr) | glove.sync_time() |
| Glove hand model path | wuji_glove_get_hand_model_path / wuji_glove_set_hand_model_path | glove.hand_model_path().get() / .set(path) |
| SDK user management | wuji_create_user / wuji_switch_user / wuji_list_users | manager.create_user() / switch_user() / list_users() |
| User data export and import | wuji_user_data_export / wuji_user_data_preview / wuji_user_data_import | manager.export_user_data() / preview_user_data() / import_user_data() |
| Glove IK calibration (async) | wuji_glove_calibration_start + wuji_glove_calibration_try_finish | glove.calibrate() |
| Glove IK calibration (sync) | wuji_glove_calibrate | glove.calibrate_blocking() |
| Glove tactile contact calibration (sync) | wuji_glove_calibrate_tactile_blocking | glove.calibrate_tactile_blocking() |
| Disconnect | wuji_dev_disconnect + wuji_dev_release | device.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/ (EMF rate divider, custom hand model for online IK, SDK user management, and IK calibration), examples/c/wuji_hand_2/ (subscribe to joint state / control actions / device info), and examples/c/wuji_hand/ (subscribe / publish / grasp loop / 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.