SDK 接口

Wuji Hand 2 的 Python 语义化 API。统一模式:hand.{resource}().{action}()。所有操作都针对整只手(20 关节)。

本页是接口参考,按需检索。单位约定、MIT 控制律、关节表与参数安全边界等控制方法见控制指南

本页自包含:既覆盖 Wuji Hand 2 专属的语义化 API,也纳入操作 Wuji Hand 2 会用到的通用 SDK 能力——安装、连接、订阅、时间同步、录制与排查,不必跳转其他产品文档。SDK 基于通用的 Wuji SDKwuji_sdkpip install wuji-sdk),源码见 wuji-sdk 仓库。完整 C API 参考与手部重定向仍在 Wuji SDK 文档

安装与环境

系统要求

项目要求
操作系统Ubuntu 22+
网络以太网(与 Wuji Hand 2 同一网段)
Python3.10+
C 编译器gcc / clang,仅使用 C 接口时需要

安装

语言安装方式
PythonPyPI 包 wuji-sdkpip install wuji-sdk
CRelease 页面 下载对应平台 tarball wuji-sdk-c-<version>-<target>.tar.gz,解压后含 lib/libwuji_sdk_c.soinclude/wuji_sdk.h

C 接口的初始化与约定见 C 接口概要

连接

网络连接前提:Wuji Hand 2 使用静态 IP,不使用 DHCP。设备按左右手出厂分配固定地址——左手 192.168.1.110,右手 192.168.1.111,网关 192.168.1.1,子网掩码 255.255.255.0。首次连接前将电脑对应网卡设置为同一网段(如 192.168.1.100),再按地址连接。出厂 IP 可后续修改,见 ip(SET/GET)

设备发现

SdkManager.scan() 扫描局域网内的 Wuji 设备:

from wuji_sdk import SdkManager

manager = SdkManager.instance()
devices = manager.scan()

for dev in devices:
    print(f"SN: {dev.sn}, 类型: {dev.device_type}, 地址: {dev.address}")

返回的 DiscoveredDevice 包含设备序列号、地址和设备类型。device_typeDeviceType 枚举,Wuji Hand 2 取 WujiHand2,连接前即可判断设备类型。Unknown 表示扫描时未获取到类型或当前 SDK 版本不识别。

通过 SdkManager 连接(推荐)

from wuji_sdk import SdkManager, Handedness

manager = SdkManager.instance()

# 自动发现并连接局域网内首个 Wuji Hand 2
hand = manager.auto_connect(device_name="wuji_hand_2")

# 已知序列号或地址时显式连接
hand = manager.connect(sn=<device_sn>, device_name="wuji_hand_2")
hand = manager.connect(address="192.168.3.110:50001", device_name="wuji_hand_2")

# 或按左右手直接连接,无需序列号
hand = manager.connect(handedness=Handedness.Right, device_name="wuji_hand_2")

auto_connectconnectdevice_name="wuji_hand_2" 重载会直接返回 WujiHand2 实例(带类型提示)。handednesssnaddress 互斥。多只同手性 Hand 2 在网时会抛 AmbiguousHandedness,请改用 sn 指定。

ConnectOptions

from wuji_sdk import SdkManager, ConnectOptions

opts = ConnectOptions(
    timeout_ms=1000,
    retry_count=3,
    enable_bridge=True,    # 默认 True:允许多个客户端(Wuji Studio + 录制脚本 + 业务程序)同时连接
)
hand = manager.connect(sn=<device_sn>, device_name="wuji_hand_2", options=opts)

enable_bridge=False 切换成独占单客户端模式。

实例属性

属性类型说明
serial_numberstr设备序列号
device_namestr连接时指定的名字(默认 "wuji_hand_2"
infoOptional[DeviceInfo]设备信息:serial_numberfirmware_version
is_connectedbool连接状态

hand.hw_version().get() 返回出厂硬件版本 HwVersion(major, minor, patch)0.0.0 表示未在工厂端写入。

多设备管理

同一进程可同时连接左右手,每台设备通过 device_name 唯一标识:

left = manager.connect(handedness=Handedness.Left, device_name="left_hand")
right = manager.connect(handedness=Handedness.Right, device_name="right_hand")

# 获取已连接设备列表
all_devices = manager.get_connected_devices()
for name, device in all_devices:
    print(f"{name}: {device.serial_number}")

# 按名称获取设备
left = manager.get_device(device_name="left_hand")

左右手用不同 device_name 即可同进程并存。重复连接同一活跃设备会抛出 SessionAlreadyExists 异常,不会断开前一次连接,沿用既有句柄即可。

双手同时采集时,用 asyncio 并发消费各自的订阅:

import asyncio
from wuji_sdk import SdkManager, Handedness

async def collect(hand, name):
    sub = hand.joint_states().subscribe()
    while True:
        frame = await sub.recv_async()
        print(f"[{name}] seq={frame.header.seq}")

async def main():
    manager = SdkManager.instance()
    left = manager.connect(handedness=Handedness.Left, device_name="left_hand")
    right = manager.connect(handedness=Handedness.Right, device_name="right_hand")
    await asyncio.gather(collect(left, "left"), collect(right, "right"))

asyncio.run(main())

断开连接

# 断开指定设备
manager.disconnect(device_name="wuji_hand_2")

# 或通过设备对象断开
hand.disconnect()

断开连接后,该设备上的所有订阅会自动关闭。重新连接需要重新建立订阅。

数据订阅

Wuji Hand 2 的实时数据通过订阅机制获取,每个数据流对应一种传感器或计算结果,用语义化 API 访问。各流的语义与字段见 Hand 级资源

订阅模式

异步接收:

sub = hand.joint_states().subscribe()
frame = await sub.recv_async()  # 等待下一帧数据

同步非阻塞接收:

sub = hand.joint_states().subscribe()
frame = sub.recv()  # 无数据时返回 None

回调接收:

def on_data(frame):
    print(frame.header.seq)

sub = hand.joint_states().subscribe_with_callback(callback=on_data)
# 后台自动接收,不阻塞主线程

关闭订阅

回调订阅需要手动关闭:

sub = hand.joint_states().subscribe_with_callback(callback=on_data)
# ... 使用一段时间后
sub.close()

只订阅实际需要的数据流,不再需要的订阅及时 close() 释放资源,避免浪费带宽和 CPU。

调节订阅流输出率

sub.set_rate(frequency_hz) 调节订阅流的固件推送频率,返回设备实际生效的输出率:

sub = hand.joint_states().subscribe()

actual = sub.set_rate(200)   # 设备量化后返回实际生效值
print(f"requested 200 Hz, applied {actual} Hz")

sub.set_rate(0)              # 传 0 恢复设备默认满速
  • 需要在未关闭的订阅句柄上调用,订阅存续期间设置持续生效。
  • 只能调低,无法超过原生输出率。设备把请求量化到支持的档位(原生输出率的整数分频),返回值以实际生效为准。
  • 输出率属于流而非句柄:同一流的所有订阅共享该设置,后写生效。
  • 设置不持久化:该流的最后一个订阅关闭或设备重连后,输出率恢复设备默认满速。
  • 需要支持输出率调节的固件。固件不支持时抛 WujiException

joint_statesjoint_diagnostics 共享同一条设备流,调节任意一个,两者同时生效。

C 接口等价:wuji_sub_set_rate(sub, frequency_hz, &actual_hz),不支持的流返回 WUJI_STATUS_ERR_UNSUPPORTED

可订阅的数据流

数据流原生输出率帧类型
joint_states1000 HzJointStateFrame
joint_diagnosticsjoint_states 同一条设备流JointDiagnosticsFrame
imu100 HzImuData

关节命令是发布方向,见 joint_command(PUB)

Hand 级资源

本节覆盖整手 API(20 关节)。单关节的读取与动作通过 hand.joint(k) / hand.joints() 返回的 JointHandle 进行(见 关节遍历)。设备默认运行在 MIT 控制模式,控制模式不通过 Python API 设置。

handedness(GET)

获取左右手识别结果(left / right)。

side = hand.handedness().get()   # → "left" 或 "right"

online_joints_count(GET)

获取在线关节数(0–20)。

n = hand.online_joints_count().get()   # → int,0–20

joint_diagnostics(SUB)

关节诊断流:订阅逐关节的状态字 / 电流 / 母线电压 / 温度 / 错误码。帧变长,仅含在线关节,用每条的 nid 定位关节。本流与 joint_states 共用同一条设备流,调低其中一个的推送频率,另一个会同步降低,见 joint_states(SUB)

sub = hand.joint_diagnostics().subscribe()   # → Subscription[JointDiagnosticsFrame]
frame = await sub.recv_async()
for j in frame.joints:
    print(j.nid, j.vbus_v_fb, j.mcu_temp_c_fb, f"0x{j.error_code_current:04X}")
sub.close()

error_code_current 用静态方法 WujiHand2.describe_error(code) 解码,返回对象含 code、错误名、desccauseresolutionseverityWarning / DeferredStop / ImmediateStop / Fatal)与 clear_policyAutoClear / ManualClear / NonClearable)。用 causeresolution 确认故障原因和处理方式。未知错误码返回 None。直接打印诊断条目对象时,错误码按 16 进制显示。

关节角请用 joint_states 流的 position,诊断流不含关节角。

comm_diag(GET,1 Hz)

通信诊断:每秒返回 5 根手指的吞吐 / 错误率。

diag = hand.comm_diag().get()           # → HandCommunicationDiagnostics
for finger in diag.fingers:             # 5 根手指各自的吞吐 / 错误率
    print(finger.tx_kbps, finger.rx_kbps, finger.error_per_sec)

用于排查关节与控制板之间的通信问题。

effort_limit(SET/GET)

力矩限幅:读写各关节的力矩上限(A)。

hand.effort_limit().set(1.5)            # 全部关节设为 1.5 A
limits = hand.effort_limit().get()      # → list[Optional[float]]

写入拒绝 NaN / Inf / 负值与超出设备上限的值,整手与单关节写入同样校验。写入被拒时报错,当前生效值保持不变。上限由固件设定,常见异常与处理见脚本运行中抛出异常

mit_params(SET/GET)

MIT 阻抗参数:读写各关节的 kp / kd。

hand.mit_params().set((1.0, 0.05))           # 全部关节统一 kp / kd
hand.mit_params().set([(1.0, 0.05)] * 20)    # 长度 20 的列表逐关节设置
mp = hand.mit_params().get()                 # → list[Optional[MitParam]],离线关节为 None

写入拒绝 NaN / Inf / 负值。

clear_fault(EXEC)

清除故障:直接动作,无参数时作用于整手。

hand.clear_fault()                  # 清除全部关节故障
hand.clear_fault(joints=mask)       # 可选:长度 20 的 0/1 掩码,只清除掩码位为 1 的关节

用户零点(origin)

把当前物理位置标定为关节侧零位。关节正在运动时延迟到下次 IDLE 才生效。

hand.set_origin()                   # 整手:所有关节刷新用户零点
hand.clear_origin()                 # 整手:清除所有关节的用户零点
hand.set_origin(joints=mask)        # 可选:长度 20 的 0/1 掩码,只作用于掩码位为 1 的关节

使能 / 失能 / 急停

hand.enable()               # 使能全部关节
hand.disable()              # 失能全部关节
hand.enable(joints=mask)    # 可选:长度 20 的 0/1 掩码,作用于掩码位为 1 的关节
hand.emergency_stop()       # 急停:整手动作,不接受掩码

joint_states(SUB)

关节状态流:订阅 20 关节的实时状态帧,原生输出率 1000 Hz。可通过订阅句柄调低推送频率:sub.set_rate(hz) 返回实际生效值,传 0 恢复原生输出率,完整用法见调节订阅流输出率。本流与 joint_diagnostics 共用同一条设备流,调低其中一个的推送频率,另一个会同步降低。

推送频率调节适用于 joint_statesjoint_diagnosticsimu 流。固件与 Wuji SDK 需同批升级到支持该能力的版本,版本要求见对应发布记录。

sub = hand.joint_states().subscribe()   # → Subscription[JointStateFrame]
frame = sub.recv()                      # 同步非阻塞,None = 暂无数据
frame = await sub.recv_async()          # 异步等待
print(frame.header.seq, frame.header.timestamp_us)
for j in frame.joints:
    print(j.nid, j.position, j.velocity, j.effort)
sub.close()

回调模式:

def on_state(frame):
    print(frame.header.seq, [j.position for j in frame.joints[:4]])

cb = hand.joint_states().subscribe_with_callback(on_state)
# ...
cb.close()

position 为关节侧角度(rad),velocity 为关节侧角速度(rad/s),这是获取关节角与关节速度的唯一推荐路径。帧变长、仅含在线关节,用每条的 nid 定位关节。joint_diagnostics 流不包含关节角。

joint_command(PUB)

关节命令:发布 20 关节的位置 / 速度 / 力矩前馈。每次发送恰好 20 个 JointCommand,每个携带该关节的 position / velocity / effort

from wuji_sdk import JointCommand

pub = hand.joint_command().publish()    # → JointCommandPublisher
pub.send([JointCommand(position=p, velocity=0.0, effort=0.0) for p in positions])
pub.close()

要把人手关键点转换成这里的 20 维关节命令,用 Wuji SDK 的手部重定向

Flash 日志导出(诊断)

# 导出当前固件的运行日志(绝大多数排查场景用这个)
result = await hand.dump_hand_logs(bank="current", out_dir="./logs")
bank含义何时用
"current"当前正在运行的固件写的日志默认使用,排查现网问题
"other"升级或回滚前的固件版本残留的日志仅当需要追溯升级 / 回滚前的现象时再导出

每次调用在 out_dir 下生成一个独立的会话目录 <sn>-<unix_ts>/,其中包含每个关节的 joint{0..19}.log 及一个 sboard.log(JSONL:每行一条 {"timestamp_ms", "level", "target", "message"})。out_dir 可省略,默认为 ~/.wuji/hand_logs。返回一个 dict:

result = await hand.dump_hand_logs(bank="current")
print(result["session_dir"])    # str: 本次会话目录绝对路径
print(result["files"])          # list[str]: 已写入的 .log 文件路径

ip(SET/GET)

读写设备静态 IP 地址set 把新 IP 写入设备 flash,固件不会热切换正在运行的以太网栈,新 IP 下次重启才生效。set 之后、重启之前,get 仍返回当前生效的 IP。

hand.ip().get()                    # 读当前 IP,如 "192.168.3.110"
hand.ip().set("192.168.2.111")     # 写入 flash,重启后生效

改 IP 的完整往返:setreboot → 按新 IP 重连 → get 确认。

hand.ip().set("192.168.2.111")
hand.reboot()
manager.disconnect_all()
# 等设备重启且以太网就绪,约 8 秒
hand = manager.connect(address="192.168.2.111:50001", device_name="wuji_hand_2")
hand.ip().get()                    # → "192.168.2.111"

新 IP 重启后才生效。set 只写入 flash,不改变正在运行的连接。重启前 get 返回的仍是当前 IP。

reboot(EXEC)

重启设备。设备会断连,重启后需重新连接。常与 ip().set() 搭配,让写入 flash 的新 IP 生效。

hand.reboot()

imu(SUB)

板载 IMU 流:以 100 Hz 的原生输出率推送加速度与角速度,可通过订阅句柄调低。设备不做板载姿态融合,orientation 字段不可用,按 ROS sensor_msgs/Imu 约定以 orientation_covariance[0] = -1 标记朝向无效。数据按 IMU 传感器自身坐标系输出,各轴与手部结构的对应关系将随手部模型后续更新明确。

sub = hand.imu().subscribe()                 # → Subscription[ImuData]
s = await sub.recv_async()
a, g = s.linear_acceleration, s.angular_velocity   # m/s² 与 rad/s
print(s.header.seq, a.x, a.y, a.z, g.x, g.y, g.z)
sub.close()

ImuData 是跨设备通用类型,字段定义见 ImuData

关节遍历

JointHandle 提供 label / index(用于与 effort_limit() / mit_params() 等 20 元素返回数组做下标对应),以及单关节资源与动作。FingerHandle 用于按手指遍历关节。

方法返回类型说明
hand.joints()list[JointHandle]全部 20 个关节
hand.fingers()list[FingerHandle]全部 5 根手指
for joint in hand.joints():
    print(joint.label, joint.index)        # e.g. "thumb_S1" 0

for finger in hand.fingers():
    for joint in finger.joints():          # 每根手指 4 个关节,按 S1..S4 顺序
        print(joint.label)                 # "thumb_S1", "thumb_S2", ...

JointHandle

关节句柄:提供 label / index、单关节资源与单关节动作。

成员类型说明
labelstr属性。关节标签,格式 {finger}_S{1..4},如 "thumb_S1""pinky_S4"
indexint属性。全局索引(0–19)
effort_limit()资源(SET/GET)单关节力矩限幅(A)
error_code()资源(GET)单关节当前错误码,用 describe_error() 解码。返回整数,直接打印时自行按 f"0x{code:04X}" 格式化
status_word()资源(GET)单关节状态字
enable() / disable()动作单关节使能 / 失能
clear_fault()动作清除该关节故障
set_origin() / clear_origin()动作设置 / 清除该关节用户零点

FingerHandle

手指句柄:遍历该手指的 4 个关节。

方法返回类型说明
joints()list[JointHandle]该手指 4 个关节,按 S1..S4 顺序

查看所有资源

列出设备上所有可用的资源、参数和 Topic:

# 所有资源
for res in hand.resources():
    print(res.path)

# 可读写参数
for param in hand.params():
    print(param.path)

# 可订阅 Topic
for topic in hand.topics():
    print(topic.path)

SDK 用户管理

不同操作者共用同一台设备时,各自的本地参数会互相覆盖。SDK 支持本机用户隔离,按 ~/.wuji/sdk/users/<user_id>/ 分目录存放每位用户的参数文件。不创建用户时,参数文件直接保存到 ~/.wuji/sdk/params/。需要按操作者隔离时,创建具名用户:

from wuji_sdk import SdkManager

manager = SdkManager.instance()

# 创建用户
alice = manager.create_user("Alice", description="右手操作员")

# 切换到指定用户
manager.switch_user(alice["user_id"])

# 查看当前用户
print(manager.current_user())

# 列出所有用户
for user in manager.list_users():
    marker = "*" if user["is_default"] else " "
    print(f"{marker} {user['display_name']} ({user['user_id']})")

# 切回默认用户
manager.switch_to_default_user()

切换用户后,已连接的设备立即重新加载该用户的参数,无需重连。删除当前用户会自动切回默认用户。C 接口提供等价的用户管理调用。

数据类型

本节先列跨设备通用类型,再列 Wuji Hand 2 专属类型。

FrameHeader

每帧数据的头部信息。

字段类型说明
seqint递增序列号
timestamp_usint设备时间戳(微秒),含义见数据帧时间戳
frame_idstr坐标系 ID(如 "l_wrist"),最大 32 字符

Vector3 / Vector3F64

三维向量。Vector3 使用 f32 精度,Vector3F64 使用 f64 精度(用于 IMU 数据,ROS 兼容)。

字段类型说明
xfloatX 分量
yfloatY 分量
zfloatZ 分量

Quaternion

f64 精度的旋转四元数。

字段类型说明
xfloatX 分量
yfloatY 分量
zfloatZ 分量
wfloatW 分量

Handedness

设备手性枚举,用于按左右手连接

说明
Handedness.Left左手(序列号第 4 位为 J
Handedness.Right右手(序列号第 4 位为 K

ImuData

IMU 传感器数据,遵循 ROS sensor_msgs/Imu 约定,是 imu(SUB) 流的帧类型。

字段类型说明
headerFrameHeader帧头
orientationQuaternion朝向四元数
orientation_covariancelist[float]朝向协方差(长度 9),首元素为 -1 表示朝向不可用
angular_velocityVector3F64角速度(rad/s)
angular_velocity_covariancelist[float]角速度协方差(长度 9)
linear_accelerationVector3F64线加速度(m/s²)
linear_acceleration_covariancelist[float]线加速度协方差(长度 9)

Wuji Hand 2 未做板载姿态融合,orientation_covariance[0] 始终为 -1

JointDiagnosticsFrame / JointDiagnosticsEntry

关节诊断帧hand.joint_diagnostics().subscribe() 流的帧类型。变长,仅含在线关节。

class JointDiagnosticsFrame:
    header: FrameHeader                    # seq / timestamp_us / frame_id
    num_joints: int
    joints: list[JointDiagnosticsEntry]
    comm: Hand2CommSummary                 # 帧级通信健康摘要,1 Hz 刷新(见 Hand2CommSummary)

class JointDiagnosticsEntry:
    nid: int                               # 节点编号,跨帧定位关节
    status_word: StatusWord                # 状态字
    current: float                         # 电流 (A)
    vbus_v_fb: float                       # 母线电压 (V)
    mcu_temp_c_fb: float                   # MCU 温度 (°C)
    error_code_current: int                # 当前错误码(整数值,打印条目对象时按 16 进制显示),用 describe_error() 解码
    comm_response_rate_pct: int            # 该关节最近 1 秒的总线响应率,0–100
    comm_timeout_total: int                # 该关节累计总线超时次数

MitParam

MIT 参数mit_params().get() 返回元素。

class MitParam:
    kp: float
    kd: float

set 接受单个 (kp, kd)(应用到全部关节)或长度 20 的列表(逐关节)。get 返回 20 个元素,离线关节为 None

JointStateFrame

关节状态帧hand.joint_states().subscribe().recv() 返回值。变长,仅含在线关节。

属性类型说明
headerFrameHeaderseq / timestamp_us / frame_id
num_jointsint本帧关节数量
jointslist[JointStateEntry]关节状态数组

JointStateEntry

单关节状态

属性类型说明
nidint节点编号,跨帧定位关节
positionfloat位置(rad,关节侧)
velocityfloat速度(rad/s)
effortfloat力矩 (A)

JointCommand

关节命令joint_command().publish().send() 入参元素。

class JointCommand:
    position: float    # 位置 (rad)
    velocity: float    # 速度 (rad/s)
    effort: float      # 力矩前馈 (A)

HandCommunicationDiagnostics

通信诊断数据

class HandCommunicationDiagnostics:
    fingers: list[FingerCommunicationDiagnostics]   # 长度 5

class FingerCommunicationDiagnostics:
    tx_frame_total: int
    rx_frame_total: int
    tx_kbps: int
    rx_kbps: int
    error_per_sec: int
    crc_error_total: int
    frame_format_error_total: int
    uart_hw_error_total: int
    transfer_stats: list[TransferStats]
    nodes: list[NodeDiagnostics]    # per-node:online / ms_since_last_response / response_rate_pct

StatusWord

状态字JointDiagnosticsEntrystatus_word 字段类型,SDK 暴露的公开状态视图。

字段类型说明
ext_stateint扩展状态值(如 Init / Ready / Enabled / Stopped)
ext_state_namestrext_state 的语义名
position_limit_activebool位置限幅触发
velocity_limit_activebool速度限幅触发
current_limit_activebool电流限幅触发

Hand2CommSummary

帧级通信健康摘要JointDiagnosticsFramecomm 字段类型,同时覆盖设备内部总线与 SDK 本地端到端(以太网)两段。设备内部快照由 SDK 常驻任务以 1 Hz 自动刷新,无需手动轮询。

字段类型说明
age_msint设备内部快照距今毫秒数。65535 = 从未成功采样(设备内部字段均为 0)。65534 = 已饱和,快照距今 ≥ 65.5 秒
e2e_receivedint全部订阅流累计收帧数
e2e_lostint以太网段经序号缺口检测出的累计丢帧数
e2e_reorderedint累计乱序/迟到帧数
e2e_duplicatesint累计重复帧数
e2e_window_loss_x100int最近 1 秒窗口丢包率,0.01% 单位
rpc_totalint累计请求数。0 表示当前传输后端不提供 RPC 统计(仅 wuji-proto 后端提供,存活的 wuji-proto 连接必然已发过至少一次请求),因此此处为 0 不等于「没有发生重传」
rpc_retriesint累计请求重传次数
rpc_timeoutsint累计最终超时的请求数
comm_get_failuresint累计内部快照刷新失败次数
sdk_droppedintSDK 内部某一跳落后而丢弃的帧数(累计)——可能是应用侧的订阅消费端,也可能是 SDK 内部的流 handler。与网络丢帧 e2e_lost 语义不同

设备内部字段的判读age_ms 以及每关节的 comm_* 字段同属一份快照,由 SDK 以 1 Hz 刷新。固件升级进行期间会有意跳过这次刷新,因此短暂的陈旧值属预期状态而非故障。用 comm_get_failures 区分两种情况:age_ms 增大而 comm_get_failures 不变 = 正在有意跳过,comm_get_failures 持续增加 = 刷新本身在失败。

计数器饱和,不回绕:本表所有计数达到上限后即停在上限(16 位的 e2e_reorderede2e_duplicatesrpc_retriesrpc_timeoutscomm_get_failures 停在 65535,32 位的停在 4294967295),不会归零重新计数。1 kHz 流长时间运行时确实可能触顶,因此应按两次采样之间的增量判读,而非当作全生命周期的绝对累计值。

关节编号

手指S1S2S3S4
thumb0123
index4567
middle891011
ring12131415
pinky16171819

标签格式:{finger}_S{1..4}(如 thumb_S1pinky_S4)。含运动范围与模型命名的完整对照表见控制指南

时间同步

设备输出的每一帧数据都带时间戳。SDK 连接设备后自动完成时间同步,让设备时间戳与主机的 UTC 时钟对齐。

数据帧时间戳

数据帧共享相同的 FrameHeader,其中 timestamp_us 是 64 位微秒时间戳,含义取决于是否已完成时间同步:

  • 同步后timestamp_us 是 UTC 微秒时间戳(Unix 时间戳),可与主机时钟直接对齐
  • 同步前timestamp_us 是设备 uptime,自上电起经过的微秒数

Wuji Hand 2 设备本身没有实时时钟 (RTC),上电后从 0 开始计时。时间同步的作用就是把这个 uptime 计数对齐到真实的 UTC 时间。反馈帧的 timestamp_us 由固件在发送时刻打戳。

同步机制

SDK 采用客户端授时:设备不主动获取时间,而是由 SDK 把主机时钟下发给设备。

同步流程沿用 NTP 的四时间点模型——SDK 与设备交换一组请求/响应消息,记录四个时刻(请求发出、设备接收、设备回复、响应收到),据此算出设备时钟相对主机 UTC 的偏移量 offset_us,再下发给设备。设备据此换算:

设备时间 = uptime_us + offset_us

这是一种 PTP-like(类 PTP)的轻量同步,不依赖标准 PTP 硬件。同步精度取决于 SDK 与设备之间的网络往返时延。

周期性自动同步需要较新的固件支持。早期固件只在连接后的首次同步应用偏移量,后续的周期性同步不会刷新设备时钟。建议将固件升级到最新版本,以获得完整的时间同步效果。

自动同步

SDK 连接设备后自动维护时间同步,应用层通常无需介入:

  • 首次同步connect() 完成时立即执行一次完整同步
  • 周期同步:连接后默认每 30 秒在后台自动同步一次,对抗设备晶振漂移

后台同步的间隔通过 ConnectOptions.auto_time_sync_interval_ms 配置:

from wuji_sdk import ConnectOptions

# 默认 30 秒
options = ConnectOptions()

# 自定义为 10 秒
options = ConnectOptions(auto_time_sync_interval_ms=10_000)

# 关闭后台自动同步
options = ConnectOptions(auto_time_sync_interval_ms=None)
取值行为
不设置默认 30000(30 秒)
None关闭后台自动同步
整数(毫秒)自定义间隔,需 ≥ 100

会话关闭时同步状态自动清除,下次连接重新同步。

手动同步

需要在自选时机强制同步时(例如长录制开始前、关键操作前),调用设备的 sync_time()

result = hand.sync_time()
print(f"偏移量: {result.offset_us} us")
print(f"往返时延: {result.round_trip_us} us")
print(f"同步完成时刻: {result.synced_at_us} us")

sync_time() 是阻塞调用,触发一次完整同步并把偏移量下发到设备,返回 TimeSyncResult

字段类型说明
offset_usint设备时钟偏移量,设备时间 = uptime_us + offset_us
round_trip_usint本次同步的网络往返时延(微秒),用单调时钟测量,越小越精准
synced_at_usint本次同步完成时的主机 UTC 时间戳(微秒)

SDK 已内置 30 秒后台自动同步,大多数场景无需手动调用 sync_time()。两种情况下手动调用有意义:在时间敏感操作前强制立即同步,或通过返回的 round_trip_us 检查最近一次同步的质量。

时间戳单调性与同步精度

设备侧打戳的 timestamp_us 在所有同步场景下严格单调递增。即使主机时钟回拨,或周期同步算出反向偏移,设备也通过平滑校正避免时间戳回退。每一帧的 timestamp_us 大于上一帧,可直接用于时序排序和采样间隔计算。

同步精度主要由 SDK 与设备之间的网络往返时延决定。TimeSyncResult.round_trip_us 反映这一指标,往返时延越小,偏移量估计越精准。有线以太网直连下,往返时延通常在数百微秒量级。后台周期同步持续校正设备晶振漂移,长时间运行也能保持设备时钟与主机 UTC 对齐。

数据录制

实时采集的关节状态与诊断数据转瞬即逝——录制功能将这些数据持久化到文件,供后续离线分析、算法调试或数据集构建使用。SDK 内置录制引擎,将多通道数据同步写入 MCAP 格式文件。整体流程分三步:

  1. 创建录制器 — 用 TopicRecorder 创建录制器,选择压缩算法
  2. 注册通道 — 调用 recorder.record(sub) 注册要录制的订阅通道
  3. 开始录制 — 调用 await recorder.start(path) 启动录制,返回 RecordingHandle 控制句柄
import asyncio
from wuji_sdk import TopicRecorder

recorder = TopicRecorder(compression="lz4")
recorder.record(hand.joint_states().subscribe())
recorder.record(hand.joint_diagnostics().subscribe())

handle = await recorder.start("./data/session.mcap")
await asyncio.sleep(60)
summary = await handle.stop()

压缩选项

创建 TopicRecorder 时通过 compression 参数选择压缩算法:

选项说明
"lz4"低延迟压缩,适合实时场景(默认)
"zstd"高压缩比,适合存储归档
"none"不压缩,写入速度最快

实时采集优先用 "lz4",长时间存储归档用 "zstd"joint_states 原生 1000 Hz,属于高频通道,用 "lz4" 避免压缩成为瓶颈。

暂停与恢复

录制过程中可随时暂停和恢复。暂停期间设备数据仍在订阅,但不会写入文件。

await handle.pause()     # 暂停——数据不会写入
await handle.resume()    # 恢复录制

Episode 切换

同一个 TopicRecorder 可多次调用 start() 切换输出文件,无需重新注册通道。适合将连续采集拆分为多个独立录制片段:

# Episode 1
handle1 = await recorder.start("./data/episode_001.mcap")
await asyncio.sleep(10)
await handle1.stop()

# Episode 2——复用已注册的通道配置
handle2 = await recorder.start("./data/episode_002.mcap")
await asyncio.sleep(10)
await handle2.stop()

录制监控

录制期间可实时监控数据质量指标,也可订阅质量告警与运行状态:

async for metrics in handle.subscribe_metrics():
    print(f"Drop rate: {metrics.frame_drop_rate:.4f}")
    print(f"Jitter: {metrics.frame_jitter_us:.1f} us")
    print(f"Sync offset: {metrics.sync_offset_ms:.2f} ms")

录制引擎内置 SPC 告警机制,当质量指标持续超过阈值时,handle.subscribe_alerts() 推送告警。handle.subscribe_status() 推送当前状态、已录帧数与已用时长。三个监控流的完整字段定义见 Wuji SDK 数据结构参考

录制摘要

handle.stop() 返回 RecordingSummary,包含录制的统计信息:

summary = await handle.stop()
print(f"Total frames: {summary.total_frames}")
print(f"File size: {summary.file_size / 1024 / 1024:.2f} MB")
print(f"Duration: {summary.duration_s:.1f}s")
print(f"Drop rate: {summary.quality.frame_drop_rate:.4f}")

录制类型

TopicRecorder:MCAP 录制会话配置器。注册通道后调用 start() 开始录制。

方法参数说明
__init__()compression: str = "lz4", chunk_size: int = None创建录制器,支持 "lz4""zstd""none"
record()sub: Subscription注册订阅通道到录制器
start()output_path: str开始录制,返回 RecordingHandle

RecordingHandle:录制控制句柄,由 TopicRecorder.start() 返回。

方法返回值说明
pause()暂停录制
resume()恢复录制
stop()RecordingSummary停止录制,返回统计摘要
subscribe_metrics()MetricsStream订阅实时质量指标流
subscribe_status()StatusStream订阅录制状态流
subscribe_alerts()AlertStream订阅质量告警流

RecordingSummary:录制统计摘要,由 handle.stop() 返回。

字段类型说明
total_framesint总帧数
file_sizeintMCAP 文件大小(字节)
duration_sfloat录制时长(秒)
qualityQualitySummary质量统计摘要

QualitySummary:录制质量汇总统计,RecordingSummary.quality 的类型。

字段类型说明
total_framesint总帧数
dropped_framesint丢失帧数
frame_drop_ratefloat丢帧率(0.0~1.0)
avg_sync_offset_msfloat平均同步偏差(毫秒)
max_sync_offset_msfloat最大同步偏差(毫秒)
sync_ratefloat同步成功率(0.0~1.0)
spc_alert_countintSPC 告警次数
duration_sfloat录制时长(秒)

异常处理

所有 SDK 操作错误统一抛 WujiException,消息中携带错误类型前缀(DisconnectedTimeoutPathNotFoundSchemaMismatchSerializeError、…)。业务侧按需 try / except 即可。

from wuji_sdk import WujiException

try:
    hand.joint_states().subscribe()
except WujiException as e:
    print(f"SDK error: {e}")

常见错误类型

错误说明
DeviceNotFound未找到指定设备
DeviceMismatch设备类型不匹配
Disconnected设备已断开连接
ConnectionTimeout连接超时
OperationTimeout操作超时
PathNotFound资源路径不存在
SchemaMismatch数据类型不匹配
NoData无可用数据
StreamClosed订阅流已关闭

按错误类型分支处理:

from wuji_sdk import SdkManager, WujiException

try:
    manager = SdkManager.instance()
    hand = manager.auto_connect(device_name="wuji_hand_2")
    sub = hand.joint_states().subscribe()
    frame = sub.recv()
except WujiException as e:
    error_msg = str(e)
    if "DeviceNotFound" in error_msg:
        print("未找到设备,请检查连接")
    elif "Disconnected" in error_msg:
        print("设备已断开")
    elif "Connection timeout" in error_msg:
        print("连接超时,请检查网络和 IP 设置")
    elif "Operation timeout" in error_msg:
        print("操作超时,设备无响应,正在重试...")
    else:
        print(f"错误: {error_msg}")

设备故障码

关节诊断的 error_code_current 与错误历史记录的 error_code 都是 u16 设备故障码,用 WujiHand2.describe_error() 解码。

设备故障码按 0xSCNN 布局,但不要自行拆解 hex 位——describe_error() 返回的 severityclear_policy 字段以字符串形式携带同样信息,布局变化时仍然正确。把码值当作不透明标识:记录日志、展示给用户、写进故障报告。解码成功时用返回的 severityclear_policy 判断处置方式。describe_error() 返回 None(未知码)时不要访问这些字段,保留原始数值用于记录与上报。

重连策略

设备断开后需要重新连接和订阅:

import time
from wuji_sdk import SdkManager, WujiException

manager = SdkManager.instance()

def connect_with_retry(sn, max_retries=5):
    for attempt in range(max_retries):
        try:
            return manager.connect(sn=sn, device_name="wuji_hand_2")
        except WujiException:
            wait = 2 ** attempt  # 指数退避
            print(f"连接失败,{wait}s 后重试...")
            time.sleep(wait)
    raise RuntimeError("达到最大重试次数")

日志与排查

本节覆盖 SDK 侧的排查手段。设备侧现象(无法发现设备、IP 冲突、关节离线、使能失败、过温过流等)见故障排查

SDK 日志级别

调整 SDK 日志级别用于调试:

import wuji_sdk

wuji_sdk.set_log_level("debug")   # 显示调试信息
wuji_sdk.set_log_level("trace")   # 显示所有日志
wuji_sdk.set_log_level("info")    # 默认级别
wuji_sdk.set_log_level("warn")    # 仅警告
wuji_sdk.set_log_level("error")   # 仅错误
wuji_sdk.set_log_level("off")     # 关闭日志

设备固件的运行日志用 Flash 日志导出 取回。

SDK 安装失败

  1. 确认 Python 版本 ≥ 3.10
  2. 使用 pip install --upgrade pip 更新 pip
  3. 遇到编译错误时,尝试安装预编译的 wheel 包

脚本运行中抛出异常

异常消息原因处理
DeviceNotFound设备未发现检查物理连接和网络
Disconnected连接中断检查线缆,重新连接
ConnectionTimeout连接超时检查网络连接或增加 timeout_ms 参数
OperationTimeout操作超时增加 timeout_ms 参数或检查设备状态
StreamClosed订阅流关闭设备可能断开,重新连接
SessionAlreadyExists同一设备已建立会话沿用既有句柄,或先 disconnect() 后重连
ValueError写入 hand.mit_params()kp / kd 均要求非负)、hand.effort_limit()(非负)或 joint_command 实时指令时传入 NaN / Inf / 负值在写入前过滤非法值
ValueError写入 hand.effort_limit() 的值超出设备当前允许上限,设备拒绝该次写入调低写入值。上限由固件设定,读回 effort_limit 确认当前生效值

订阅数据延迟或丢帧

  • 检查网络带宽和延迟(有线连接优于 WiFi)
  • 减少同时订阅的数据流数量
  • 确认回调函数中没有耗时操作阻塞接收
  • 订阅 hand.joint_diagnostics(),其帧级 comm 摘要能定位丢帧发生在哪一段:e2e_lost 是网络丢帧,sdk_dropped 是消费端处理不及时。字段定义见 Hand2CommSummary

SDK 版本与固件版本不兼容

  • 查看 SDK 版本发布记录 了解版本对应关系
  • 建议 SDK 和固件保持同一主次版本(如均为 0.6.x)

C 接口概要

SDK 另提供 C 接口(libwuji_sdk_c.so + wuji_sdk.h),与 Python 接口语义一致。本节给出初始化、调用约定与两套接口的对应关系,完整的 C 结构体、typed 回调与函数签名见 Wuji SDK 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 — 关闭订阅句柄

使用约束(不遵守会出现 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)。

与 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_sub_set_rate(sub, hz, &actual)sub.set_rate(hz)
控制动作(含 20 关节掩码)wuji_hand_2_enable(dev, mask)hand.enable(joints=mask)
实时指令wuji_hand_2_joint_command_publish + wuji_joint_command_publisher_sendhand.joint_command().publish().send([...])
关节状态订阅wuji_hand_2_subscribe_joint_stateshand.joint_states().subscribe()
关节诊断订阅wuji_hand_2_subscribe_joint_diagnosticshand.joint_diagnostics().subscribe()
IMU 订阅wuji_hand_2_subscribe_imuhand.imu().subscribe()
错误码描述wuji_hand_2_describe_error(code, &info)WujiHand2.describe_error(code)
SDK 用户管理wuji_create_user / wuji_switch_user / wuji_list_usersmanager.create_user() / switch_user() / list_users()
断开wuji_dev_disconnect + wuji_dev_releasedevice.disconnect()

CMake 工程示例与最小链接方式见 C 接口参考 · CMake 工程示例

Beta 1 设备的接口可用性

本页接口在 Beta 1 与 Beta 2 设备上通用,以下能力受硬件条件限制,在 Beta 1 设备上升级固件与 SDK 后仍不可用或行为不同:

接口 / 能力Beta 1 设备说明
手背状态灯相关行为需相应硬件批次未配备状态灯硬件的 Beta 1 设备无灯效输出,接口调用不报错
Hand2CommSummary 的设备内部字段依赖固件版本具体版本门槛暂不提供,将在后续更新

完整的硬件与软件对应关系见版本识别与兼容性

订阅更新