C 接口参考

Wuji SDK 提供 C 接口(libwuji_sdk_c.so + wuji_sdk.h),与 Python 接口语义一致——设备发现、连接、按设备类型的 typed 订阅回调、全局坐标变换、SDK 用户管理与 Wuji Glove IK 标定。安装方式与 Python 并列见 产品介绍

完整 typed 回调与结构体定义参见 SDK tarball(从 wuji-sdk Releases 下载)解压后的 include/wuji_sdk.h,配套 CMake 工程示例与构建说明见 wuji-sdk 仓库 examples/c/ 目录

初始化与约定

初始化

进程级初始化一次,结束时释放:

#include "wuji_sdk.h"

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

// ... 业务逻辑

wuji_shutdown();

返回值分类

  • WujiStatus — 多数操作(wuji_init / wuji_scan / wuji_connect / 订阅打开等),WUJI_STATUS_OK 表示成功,失败时通过 wuji_last_error() 取当前线程错误描述。
  • int32_t — 元信息读取(wuji_dev_serial_number / wuji_dev_device_name)返回写入字节数。
  • uint8_t — 状态查询 wuji_dev_is_connected
  • voidwuji_shutdown 与资源释放函数。

资源释放函数

  • wuji_discovered_freewuji_scan 后释放设备列表
  • wuji_dev_release — 与 wuji_connect 配套,断开后释放设备句柄
  • wuji_sub_close — 关闭订阅句柄

设备发现与连接

扫描局域网内的 Wuji 设备:

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

扫描结果带设备类型:device_idWujiDeviceType 枚举,与 WUJI_DEVICE_TYPE_* 常量(_WUJI_GLOVE / _WUJI_HAND_2 / _WUJI_HAND / _UNKNOWN)比较即可在连接前判断设备类型,model 为对应的类型字符串。_UNKNOWN 表示扫描时未获取到类型或当前 SDK 版本不识别。

按 SN 或地址连接(target.kindWUJI_CONNECT_TARGET_KIND_SN_ADDR,本地别名作为 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);

// ... 业务

wuji_dev_disconnect(dev);
wuji_dev_release(dev);

device_name 与 Python 语义一致:本地别名,不参与设备匹配,不决定返回类型,要求非空且不含 /.。详见 设备连接

WujiConnectOptions 字段与 Python ConnectOptions 对齐:enable_bridge(默认 true)控制是否允许多个 SDK 实例连接同一设备,auto_time_sync_interval_ms / auto_time_sync_interval_enabled 配置后台时间同步(默认 30000 毫秒,关闭后 connect() 内的首次同步仍执行)。推荐使用 wuji_connect_options_default() 取默认值再覆盖需要的字段——零初始化整个结构体会把这些字段一并置零,改变连接行为。不需要定制时给 wuji_connectNULL 即可。

SDK 用户管理

C SDK 提供与 Python 对齐的本机用户隔离接口。具名用户的标定产物与设备参数按 ~/.wuji/sdk/users/<user_id>/ 独立存放。默认用户不同:它的设备参数放在共享的 ~/.wuji/sdk/params/ 下,也不支持标定与自定义手部模型。切换用户后,已连接设备的参数存储立即重新加载。隔离模型与目录布局见 设备参数与用户隔离

创建用户并切换:

WujiUserInfo created = {0};
wuji_create_user("Alice", "右手操作员", /*external_id=*/NULL, &created);

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

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

列举用户、读当前用户、切回默认用户:

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 ? " [默认]" : "");
}
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 含 7 个字段:user_iddisplay_namedescriptionexternal_idis_defaultcreated_atupdated_at。更新用 wuji_update_user,通过 WujiUserUpdate 只传要改的字段,置位 clear_description / clear_external_id 可清空对应值:

const char* user_id = "usr_xxxxxxxx";   // 已有用户的 ID(来自 wuji_create_user / wuji_list_users)

WujiUserUpdate update = {
    .display_name = "Alice R.",
    .clear_description = true,   // 清空描述
};
WujiUserInfo updated = {0};
wuji_update_user(user_id, &update, &updated);
wuji_user_info_free(&updated);

wuji_delete_user(user_id);

WujiUserInfo 持堆字符串:单个结构用 wuji_user_info_free 释放,wuji_list_users 返回的数组用 wuji_user_info_array_free。默认用户不支持标定与自定义手部模型,需先创建并切换到具名用户,详见 设备参数与用户隔离

用户数据导入导出

把一位 SDK 用户的全部数据(手部标定模型、触觉模型与最新一次完整的触觉标定数据)打包成一个可迁移的 .zip,用于备份或换机。三个接口与 Python 行为一致,语义见 用户数据导入导出

// 导出:user_id 传 "" 表示默认用户,导出路径必须以 .zip 结尾
if (wuji_user_data_export("usr_xxxxxxxx", "alice.zip") != WUJI_STATUS_OK) {
    fprintf(stderr, "export failed: %s\n", wuji_last_error());
}

// 预览:跑完整校验但不写盘,返回 OK 表示这个包可以导入
if (wuji_user_data_preview("alice.zip") != WUJI_STATUS_OK) {
    fprintf(stderr, "bundle rejected: %s\n", wuji_last_error());
}

// 导入:恢复到包内记录的所属用户(不存在则创建),当前用户不变
if (wuji_user_data_import("alice.zip") != WUJI_STATUS_OK) {
    fprintf(stderr, "import failed: %s\n", wuji_last_error());
}

三个接口只返回 WujiStatus,失败原因用 wuji_last_error() 取。两个状态码专门用来区分包的问题:

  • WUJI_STATUS_ERR_NOT_FOUND — 包文件不存在
  • WUJI_STATUS_ERR_INVALID_DATA — 包在但校验不通过,例如 zip 损坏、manifest 非法、sha256 不匹配、条目路径不安全或超出大小上限

校验在写盘前完成,任一项失败都不会留下任何文件。逐个组件的明细报告不跨 FFI 传递,需要按组件查看导入结果时改用 Python 或 Rust 接口。

订阅设备数据流

每种 typed 数据流都有专用包装 wuji_<device>_subscribe_<name>,回调签名与数据 schema 绑定。以 Wuji Glove 触觉为例:

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

// ... 回调在后台触发

wuji_sub_close(sub);

Wuji Glove 暴露的 typed 订阅入口:tactiletactile_zonestactile_binarytactile_residualtactile_point_cloudemf_poseshand_joint_angleshand_skeletontip_posesimu_palm 及五指 imu_<finger> / imu_data_<finger>。Wuji Hand 2 暴露 subscribe_joint_statessubscribe_joint_diagnosticssubscribe_imujoint_states / joint_diagnostics 帧变长、仅含在线关节,按 nid 字段识别每条。Wuji Hand(一代)暴露 subscribe_joint_statesWujiHandJointStates,定长 20 关节)以及配对触觉手套的 subscribe_tactile_pressure_frame / subscribe_tactile_statusWujiTactileGlove* 类型),详见 Wuji Hand C 接口

Wuji Hand 2 C 接口

Wuji Hand 2 在 C 侧镜像 Python WujiHand2 资源式接口。控制动作支持 20 关节掩码,整手反馈走订阅流(变长、仅含在线关节),实时指令通过 publisher 推送结构数组。

控制动作(含 20 关节掩码)

enable / disable / clear_fault / set_origin / clear_origin 接受可选掩码 const uint8_t mask[20],传 NULL 表示对全部关节触发。emergency_stop 仅作用于整手、不接受掩码:

// 整手使能
wuji_hand_2_enable(dev, NULL);
// 整手清错
wuji_hand_2_clear_fault(dev, NULL);

// 仅食指 4 关节(flat 索引 4..7)
uint8_t mask[20] = {0};
for (int i = 4; i < 8; i++) mask[i] = 1;
wuji_hand_2_enable(dev, mask);            // 仅使能食指 4 关节
wuji_hand_2_clear_fault(dev, mask);       // 仅清食指 4 关节的故障

// 整手紧急停止
wuji_hand_2_emergency_stop(dev);

// 用户零点
wuji_hand_2_set_origin(dev, NULL);
wuji_hand_2_clear_origin(dev, NULL);

MIT 参数与力矩上限(含在线 bitmap)

整手配置写入支持统一值与 per-joint 数组:

// 整手 effort_limit 统一 1.5 A
wuji_hand_2_set_all_effort_limit(dev, 1.5f);

// per-joint 写入(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);

读取返回 flat-20 数组 + 在线 bitmap,离线关节槽位为零(mit_params 离线为 NaN):

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]);
    }
}

写入对 NaN / Inf / 负值返回 WUJI_STATUS_ERR_INVALID_ARG,在写入前应过滤非法值。

订阅整手反馈流

变长帧含 FrameHeaderseq + timestamp_us + frame_id = "l_wrist" | "r_wrist"),joints 数组按 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);
// ... 关闭:wuji_sub_close(sub);

wuji_hand_2_subscribe_joint_diagnostics 回调签名相似,每条 WujiJointDiagnosticsEntrystatus_word / current / vbus_v_fb / mcu_temp_c_fb / error_code_current 字段,以及每关节总线通信质量(comm_response_rate_pct / comm_timeout_total)。帧级 comm 字段(WujiHand2CommSummary)附带端到端流丢帧、RPC 重传/超时计数与触觉在线位图。wuji_hand_2_subscribe_imu 回调 WujiImuData(与 Wuji Glove imu_* 同 schema)。

实时指令 publisher

打开 publisher 后调用 send,传入恰好 20 个 WujiJointCommand(每项 position / velocity / effort),不需要前馈的字段填 0:

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

按固定频率循环调 send 维持持续控制(典型 200 Hz–1 kHz)。close 对 NULL 安全,但 publisher 句柄仍需调用 wuji_joint_command_publisher_close 释放,否则会泄漏资源。

错误码描述

wuji_hand_2_describe_error 是静态函数(无需设备连接),把错误码(来自 joint_diagnosticserror_code_current)解码到 WujiErrorInfo

WujiErrorInfo info = {0};
// 0x2102 = Overcurrent(ImmediateStop / ManualClear),生产中真实可能命中的故障码
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);
}

未知错误码返回 WUJI_STATUS_ERR_NOT_FOUNDseverity 取值:Warning / DeferredStop / ImmediateStop / Fatalclear_policy 取值:AutoClear / ManualClear / NonClearable

身份与诊断

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

// 字符串两步查询:先用 buf=NULL,buf_len=0 探出所需长度
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 返回的 out 持有堆字段,使用后需调对应 _free 释放(仅在返回 WUJI_STATUS_OK 时)。

Wuji Hand C 接口

Wuji Hand(一代,USB 直连)同样接入 C SDK,接口形态对齐 Wuji Hand 2——指令结构、订阅回调与函数命名一致,控制代码可在两代手之间直接迁移。

Wuji Hand C 接口仅在 Linux x86_64 / aarch64(gnu)tarball 可用:包内只有 libwuji_sdk_c.so 一个库文件,不再依赖 libstdc++.so.6libusb-1.0.so.0。Android 包中这些函数存在但返回 WUJI_STATUS_ERR_UNSUPPORTED

从早期 tarball 部署过 libwujihandcpp.so 的话,可以删掉,SDK 不再加载它。C 接口与链接方式没变,现有程序无需重新编译。

连接与整手控制

wuji_hand_connect_sn 按 USB 序列号连接(等价于 wuji_connect 的 SN 方式)。控制动作作用于整手,不接受 20 关节掩码:

// 先扫描发现在线设备及其 USB 序列号
WujiDiscovered* devs = NULL;
size_t count = 0;
wuji_scan(&devs, &count);

// 按设备类型筛出目标一代 Hand(device_id == WUJI_DEVICE_TYPE_WUJI_HAND),连接后再释放列表
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) {                            // 未发现一代 Hand,或连接失败
    return 1;
}

wuji_hand_enable(dev);                        // 使能全部 20 关节
wuji_hand_set_all_effort_limit(dev, 1.5f);    // 力矩上限(安培)

float pos[20];
wuji_hand_read_joint_state(dev, pos);         // 20 关节位置快照

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

wuji_hand_clear_all_faults 清整手故障。诊断与元信息读取:wuji_hand_get_all_diagnostics(母线电压、温度、错误码)、wuji_hand_get_soft_limits(固件软限位上下界)、wuji_hand_get_handednesswuji_hand_get_firmware_version / get_product_sn / get_input_voltage / get_temperaturewuji_hand_joint_label / wuji_hand_finger_name 返回关节与手指命名(静态函数,无需连接)。

两个 handedness getter 都返回原始 uint8_t,但编码彼此相反,不能混用。wuji_hand_get_handedness0 = 右、1 = 左,与 WujiHandedness 枚举相反,直接套枚举会把左右颠倒。wuji_hand_get_tactile_handedness0 = 左、1 = 右,与枚举一致。

实时指令与控制器

publisher 与 Wuji Hand 2 同构:send 一次读取恰好 20 个 WujiJointCommand。低通滤波实时位置控制器支持读回实际位置与实际力矩:

// 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);

// 低通滤波实时位置控制器
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);   // 实际力矩(安培)

wuji_hand_realtime_controller_close(ctrl);

两个 get_actual_* 都从非阻塞缓存读取,不影响控制频率。

配对触觉手套

wuji_hand_is_tactile_attached 探测触觉手套是否在位。数据订阅:wuji_hand_subscribe_tactile_pressure_frameWujiTactileGloveFrame,20×31 压力网格)、wuji_hand_subscribe_tactile_statusWujiTactileGloveStatus)。信息读取:wuji_hand_get_tactile_device_info / get_tactile_diagnostics / get_tactile_handedness

手套不在位或故障时,这些调用返回错误状态,具体原因用 wuji_last_error() 读取。

Wuji Glove 资源接口

Wuji Glove 暴露与 Python 对齐的 set / get / exec 资源接口。

设备参数读写

身份与网络参数:

// 字符串 typed getter 用两步查询:先 buf=NULL 探长度,再分配填充
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);

// 写入 IP 与端口(修改后需要重连设备)
wuji_glove_set_ip(dev, "192.168.1.100");
wuji_glove_set_port(dev, 50001);

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

设备控制

// 重启设备(设备会断连,调用后需要重新 wuji_connect)
wuji_glove_reboot(dev);

EMF 输出率调节

// 读回当前 divider(默认 1,即不降频)
uint32_t divider = 0;
wuji_glove_get_emf_poses_rate_divider(dev, &divider);

// 设 N=4 → EMF poses 输出率降到 input_rate/4(120 Hz → ~30 Hz)
wuji_glove_set_emf_poses_rate_divider(dev, 4);

EMF poses 衍生流(hand_joint_angles / tip_poses / hand_skeleton / tactile_point_cloud)随之同步降频,IMU 与触觉原始流不受影响。完整说明见 Wuji Glove 数据参考

时间同步

wuji_glove_sync_time 触发一次完整往返同步,对齐 Python glove.sync_time()。结果写入 WujiTimeSyncResultoffset_us / round_trip_us / synced_at_us 3 个字段,单位均为微秒),结构为纯 POD,无堆字段、无需 free。完整语义见 时间同步与时间戳

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);
}

调用阻塞至往返完成,与 SDK 后台的周期同步任务串行,失败时保持 *out 不变。后台周期同步的间隔与开关经 WujiConnectOptionsauto_time_sync_interval_ms / auto_time_sync_interval_enabled 配置。

在线 IK 自定义手部模型

wuji_glove_set_hand_model_path 设置自定义手部 URDF 路径,wuji_glove_get_hand_model_path 读回,对齐 Python glove.hand_model_path().set(path) / .get()。设置路径后,在线 IK 数据流(hand_joint_angles / tip_poses / hand_skeleton)通过 SDK 标定生成机制按自定义 URDF 重载。getter 采用两步 size 查询——先传 NULL 获取所需长度,再次调用填充:

// 设置在线 IK 的自定义手部 URDF
wuji_glove_set_hand_model_path(dev, "/path/to/hand.urdf");

// 读回(两步 size 查询)
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);

仅具名 SDK 用户可设置自定义手部模型:默认 SDK 用户的在线 IK 始终使用内置默认 URDF,此时调用 wuji_glove_set_hand_model_path 返回错误,wuji_last_error 会提示先切换到具名用户。路径指向不可读文件时同样立即报错,不写入参数存储。

IK 标定

Wuji Glove 的 IK 标定在 C 侧提供同步与异步两种入口,对齐 Python glove.calibrate()。标定按用户隔离,需先切换到具名 SDK 用户,默认用户直接标定会返回错误。标定动作、姿势序列与产物说明见 Wuji Glove 标定

wuji_glove_calibration_options_default 取默认选项(skip_constraints 是否跳过约束校验、timeout_s 超时秒数)。反馈回调收到的 WujiGloveCalibrationFeedback 携带当前姿势进度。其中只有 stateprogress 必然存在。其余标量字段由配套的 has_* 标志位门控——has_step_indextrue 时才能读 step_indexstep_totalstep_namehold_elapsedframes_collected 等同理。metricshints 是数组,没有 has_* 位,用 metrics_len / hints_len 判断是否为空。WujiGloveCalibrationMetric 内部同样带门控:has_finger / has_finger_btrue 时才有 finger / finger_b,所以指标不一定是按手指给出的。

异步 session:start 开始后用 try_finish 轮询或 wait 阻塞等待完成,cancel 请求协作式取消,最后用 session_free 释放句柄。

void on_feedback(const WujiGloveCalibrationFeedback* fb, void* user) {
    // fb->state / fb->progress / fb->step_index / fb->metrics ...
    // fb 仅回调期间有效,需要保留的字段必须拷贝
}

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 */) 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);   // 轮询间隔约 50 ms
}
if (session) wuji_glove_calibration_session_free(session);

// 仅在启动成功、标定正常完成时读取结果;取消或失败时不要读 result 字段
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);
}

同步入口 wuji_glove_calibrate 一次调用阻塞至标定完成,参数与异步一致,直接写 WujiGloveCalibrationResult

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

WujiGloveCalibrationResult 含采集姿势数 poses_collected、每姿势帧数 frames_per_pose、手别 handedness、标定生成的 URDF 路径 calibrated_urdf,以及标定所属用户快照 sdk_usercalibrated_urdf 是 SDK 分配的本地路径,按不透明值处理,不要依赖其父目录结构。结果用完必须调 wuji_glove_calibration_result_free 释放堆字段。

取消是协作式的。wuji_glove_calibration_cancel 返回 WUJI_STATUS_OK 只代表请求已记录,不代表标定已停下。只有取消在结果发布前生效,本次标定才映射为 WUJI_STATUS_ERR_CANCELLED。一旦标定进入发布阶段,它仍可能成功完成或返回发布阶段的错误,因此请以 session 实际返回的状态码为准,不要默认取消后一定拿到 ERR_CANCELLED。设备不支持标定时映射为 WUJI_STATUS_ERR_UNSUPPORTED

Glove 触觉接触标定

wuji_glove_calibrate_tactile_blocking 一次调用完成引导式触觉接触标定:按提示录制几组动作,SDK 依次完成采集、数据校验与训练。数据流说明见 Wuji Glove 触觉数据

调用前先决定 install

  • install = true:训练后执行加载校验并安装,模型按当前 SDK 用户与手套序列号保存,订阅 tactile_binary / tactile_residual 时自动加载。可选灵敏度参数(has_sensitivity 置 true 并填 sensitivity)仅此模式接受,否则调用直接报错。
  • install = false:只训练不安装,跳过加载校验与安装,产物保存在本次运行目录,不会自动加载。适合先离线评估训练效果、再决定是否安装的场景。

调用方式:

// 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,        // 采集与训练进度
    .on_pose_prompt = on_pose_prompt,  // 每组动作前后交互:继续、重录或停止
};
WujiTactileCalibrationSummary summary = {0};
if (wuji_glove_calibrate_tactile_blocking(dev, &options, &callbacks, &summary) == WUJI_STATUS_OK) {
    // 读取 summary 后释放堆字段
    wuji_tactile_calibration_summary_free(&summary);
}

on_pose_prompt 让流程可交互:每组动作开始前与录制结束后各回调一次,参数为 WujiTactilePromptRequest,返回 0 继续、1 重录、2 停止,其他取值一律按停止处理。on_feedback 上报采集与训练进度,返回 0 继续、1 中止,其他取值一律按中止处理。两个回调都可不设置,此时全程自动执行。回调收到的事件指针仅在回调期间有效。timeout_s 是协作式超时:超时触发取消,但函数会先等在途的采集或训练与正在执行的回调返回,因此函数返回后不再有任何回调触发。若 on_pose_prompt 内部无限阻塞(如等待控制台输入),超时无法强制它返回。

成功返回后从 summary 读取结果,model_dir 两种取值按 installed 区分:

  • installed = truemodel_dir 指向已安装的模型目录,订阅时自动加载。
  • installed = falsemodel_dir 指向本次运行保存的模型,不会被自动加载。

verified_alive_taxels 仅在 has_verified_alive_taxels 为 true 时有效。读完调用 wuji_tactile_calibration_summary_free 释放堆字段。

失败原因通过 wuji_last_error() 获取。库构建时未启用 model-export 特性会返回 WUJI_STATUS_ERR_UNSUPPORTED。Python 对应接口为 glove.calibrate_tactile_blocking()(阻塞)。异步版 glove.calibrate_tactile() 仅 Python 提供,C 侧无异步入口。

订阅全局资源

跨设备聚合的坐标变换通过 manager.tf() 在 Python 暴露,C 等价为 device-less 包装:

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);
// 同样:wuji_subscribe_tf_static(...)

wuji_sub_close(sub);

重定向

把 21 个人手关键点(MediaPipe 顺序)映射为固件顺序的 20 维关节角,与 Python RetargetSession 行为一致。概念与输入格式见手部重定向

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,行主序 xyz,单位米,MediaPipe landmark 顺序
float qpos[20];        // 固件顺序关节角(rad)
/* fill keypoints from your source */
st = wuji_retarget_session_step(session, keypoints, qpos);
if (st != WUJI_STATUS_OK) {
    // 退化 / 非法关键点帧返回 WUJI_STATUS_ERR_ALGORITHM,本帧丢弃即可
    fprintf(stderr, "step failed: %s\n", wuji_last_error());
}
// 切换数据源或跟踪中断后重置 warm-start 与滤波状态:
wuji_retarget_session_reset(session);

wuji_retarget_session_free(session);   // 传 NULL 为 no-op
  • modelWUJI_HAND_MODEL_WUJI_HAND / WUJI_HAND_MODEL_WUJI_HAND2sideWUJI_HANDEDNESS_LEFT / WUJI_HANDEDNESS_RIGHT
  • 退化或非法的关键点帧返回 WUJI_STATUS_ERR_ALGORITHM,详情用 wuji_last_error() 获取
  • 完整示例(含 Wuji Glove 输入 → 重定向 → 驱动实机的遥操作)见 examples/c/retargeting/

使用约束

C SDK 用户必须遵守以下约束,否则会出现 use-after-free、死锁或读到失效错误描述:

  • typed 回调的 frame 指针仅在回调期间有效,返回后堆字段即被释放。需要保留的数据必须在回调内拷贝出来,不得保存指针。
  • 不要在订阅自身的回调里调 wuji_sub_closeclose 会 join worker 线程,在自身回调中调用必死锁。请在外部线程上 close。
  • wuji_last_error() 返回值仅在同线程下一次 wuji_* 调用前有效。需要保留的错误描述应立即拷贝。
  • END / ERROR 是终止帧,回调收到这两类帧后流不再有新数据,但仍需调用 wuji_sub_close 释放订阅句柄。
  • 字符串 getter 使用两步查询:先用 buf=NULL, buf_len=0 拿到 *needed(所需字节,含 NUL),再分配缓冲第二次调用填充。直接传不足缓冲会返回 WUJI_STATUS_ERR_BUFFER_TOO_SMALL,不做截断。
  • 整手批量读(get_all_effort_limit / get_all_mit_params)返回 flat-20 数组 + 在线 bitmap,用 WUJI_JOINT_ONLINE(mask, i) 宏判断槽位是否有效,否则读到离线关节的占位值(0 或 NaN)。
  • 标定反馈回调在标定 worker 线程触发,feedback 指针仅回调期间有效。回调内调用 session 接口不会死锁,SDK 会拦下来:wuji_glove_calibration_waitwuji_glove_calibration_try_finish 返回 WUJI_STATUS_ERR_INVALID_ARGwuji_glove_calibration_session_free 把原因记进 wuji_last_error() 并跳过这次释放——session 仍然存活,必须在回调返回后再释放一次。wuji_glove_calibration_cancel 可以在回调内安全调用。session 用完调 wuji_glove_calibration_session_free,标定结果用完调 wuji_glove_calibration_result_free
  • WujiUserInfo 持堆字符串:单个结构用 wuji_user_info_freewuji_list_users 返回的数组用 wuji_user_info_array_free

Python 接口对照

功能CPython
初始化wuji_init(NULL)SdkManager.instance()
扫描wuji_scan(&devs, &count)manager.scan()
连接wuji_connect(&target, alias, &opts, &dev)manager.connect(...) / manager.auto_connect(...)
连接选项默认值wuji_connect_options_default()ConnectOptions()
设备订阅wuji_glove_subscribe_tactile(dev, cb, user, &sub)glove.tactile().subscribe_with_callback(...)
全局订阅wuji_subscribe_tf(cb, user, &sub)manager.tf().subscribe()
Hand 2 控制动作wuji_hand_2_enable(dev, mask)hand.enable(joints=mask)
Hand 2 实时指令wuji_hand_2_joint_command_publish + wuji_joint_command_publisher_sendhand.joint_command().publish().send([...])
Hand 2 关节状态订阅wuji_hand_2_subscribe_joint_stateshand.joint_states().subscribe()
Hand 2 关节诊断订阅wuji_hand_2_subscribe_joint_diagnosticshand.joint_diagnostics().subscribe()
Hand 2 IMU 订阅wuji_hand_2_subscribe_imuhand.imu().subscribe()
Hand 2 错误码描述wuji_hand_2_describe_error(code, &info)WujiHand2.describe_error(code)
Hand 连接(USB SN)wuji_hand_connect_sn(sn, alias, &dev)WujiHand.connect_sn(sn, alias)
Hand 关节状态订阅wuji_hand_subscribe_joint_stateshand.joint_states().subscribe()
Hand 实时指令wuji_hand_joint_command_publish + wuji_hand_joint_command_publisher_sendhand.joint_command().publish().send([...])
Hand 实时控制器wuji_hand_realtime_controller_open / _set_target_position / get_actual*hand.realtime_controller(LowPass(...))
Glove EMF 降频wuji_glove_set_emf_poses_rate_divider(dev, N)glove.emf_poses_rate_divider().set(N)
Glove 时间同步wuji_glove_sync_time(dev, &tsr)glove.sync_time()
Glove 手部模型路径wuji_glove_get_hand_model_path / wuji_glove_set_hand_model_pathglove.hand_model_path().get() / .set(path)
SDK 用户管理wuji_create_user / wuji_switch_user / wuji_list_usersmanager.create_user() / switch_user() / list_users()
用户数据导入导出wuji_user_data_export / wuji_user_data_preview / wuji_user_data_importmanager.export_user_data() / preview_user_data() / import_user_data()
Glove IK 标定(异步)wuji_glove_calibration_start + wuji_glove_calibration_try_finishglove.calibrate()
Glove IK 标定(同步)wuji_glove_calibrateglove.calibrate_blocking()
Glove 触觉接触标定(同步)wuji_glove_calibrate_tactile_blockingglove.calibrate_tactile_blocking()
断开wuji_dev_disconnect + wuji_dev_releasedevice.disconnect()

CMake 工程示例

完整可运行 C 工程(含 CMakeLists.txt + 订阅回调示例)见 wuji-sdk 仓库的 examples/c/ 目录,按设备分别存放在 examples/c/wuji_glove/(EMF 降频、在线 IK 自定义手部模型、SDK 用户管理与 IK 标定)、examples/c/wuji_hand_2/(订阅关节状态 / 控制动作 / 设备信息)与 examples/c/wuji_hand/(订阅 / 发布 / 抓握循环 / 触觉状态)子目录,构建步骤参见该目录 README。最小链接方式:

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

其中 ${SDK} 是解压后的 tarball 根目录。CMake 工程通过 -DWUJI_SDK_INCLUDE_DIR-DWUJI_SDK_LIB 指向同样路径。