SDK 接口
Wuji Hand 2 的 Python 语义化 API。统一模式:hand.{resource}().{action}()。所有操作都针对整只手(20 关节)。
本页是接口参考,按需检索。单位约定、MIT 控制律、关节表与参数安全边界等控制方法见控制指南。
本页自包含:既覆盖 Wuji Hand 2 专属的语义化 API,也纳入操作 Wuji Hand 2 会用到的通用 SDK 能力——安装、连接、订阅、时间同步、录制与排查,不必跳转其他产品文档。SDK 基于通用的 Wuji SDK(wuji_sdk,pip install wuji-sdk),源码见 wuji-sdk 仓库。完整 C API 参考与手部重定向仍在 Wuji SDK 文档。
安装与环境
系统要求
| 项目 | 要求 |
|---|---|
| 操作系统 | Ubuntu 22+ |
| 网络 | 以太网(与 Wuji Hand 2 同一网段) |
| Python | 3.10+ |
| C 编译器 | gcc / clang,仅使用 C 接口时需要 |
安装
| 语言 | 安装方式 |
|---|---|
| Python | PyPI 包 wuji-sdk:pip install wuji-sdk |
| C | 从 Release 页面 下载对应平台 tarball wuji-sdk-c-<version>-<target>.tar.gz,解压后含 lib/libwuji_sdk_c.so 与 include/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_type 为 DeviceType 枚举,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_connect 和 connect 的 device_name="wuji_hand_2" 重载会直接返回 WujiHand2 实例(带类型提示)。handedness 与 sn、address 互斥。多只同手性 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_number | str | 设备序列号 |
device_name | str | 连接时指定的名字(默认 "wuji_hand_2") |
info | Optional[DeviceInfo] | 设备信息:serial_number、firmware_version |
is_connected | bool | 连接状态 |
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_states 与 joint_diagnostics 共享同一条设备流,调节任意一个,两者同时生效。
C 接口等价:wuji_sub_set_rate(sub, frequency_hz, &actual_hz),不支持的流返回 WUJI_STATUS_ERR_UNSUPPORTED。
可订阅的数据流
| 数据流 | 原生输出率 | 帧类型 |
|---|---|---|
| joint_states | 1000 Hz | JointStateFrame |
| joint_diagnostics | 与 joint_states 同一条设备流 | JointDiagnosticsFrame |
| imu | 100 Hz | ImuData |
关节命令是发布方向,见 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–20joint_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、错误名、desc、cause、resolution、severity(Warning / DeferredStop / ImmediateStop / Fatal)与 clear_policy(AutoClear / ManualClear / NonClearable)。用 cause 与 resolution 确认故障原因和处理方式。未知错误码返回 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_states、joint_diagnostics 与 imu 流。固件与 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 的完整往返:set → reboot → 按新 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、单关节资源与单关节动作。
| 成员 | 类型 | 说明 |
|---|---|---|
label | str | 属性。关节标签,格式 {finger}_S{1..4},如 "thumb_S1"、"pinky_S4" |
index | int | 属性。全局索引(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
每帧数据的头部信息。
| 字段 | 类型 | 说明 |
|---|---|---|
seq | int | 递增序列号 |
timestamp_us | int | 设备时间戳(微秒),含义见数据帧时间戳 |
frame_id | str | 坐标系 ID(如 "l_wrist"),最大 32 字符 |
Vector3 / Vector3F64
三维向量。Vector3 使用 f32 精度,Vector3F64 使用 f64 精度(用于 IMU 数据,ROS 兼容)。
| 字段 | 类型 | 说明 |
|---|---|---|
x | float | X 分量 |
y | float | Y 分量 |
z | float | Z 分量 |
Quaternion
f64 精度的旋转四元数。
| 字段 | 类型 | 说明 |
|---|---|---|
x | float | X 分量 |
y | float | Y 分量 |
z | float | Z 分量 |
w | float | W 分量 |
Handedness
设备手性枚举,用于按左右手连接。
| 值 | 说明 |
|---|---|
Handedness.Left | 左手(序列号第 4 位为 J) |
Handedness.Right | 右手(序列号第 4 位为 K) |
ImuData
IMU 传感器数据,遵循 ROS sensor_msgs/Imu 约定,是 imu(SUB) 流的帧类型。
| 字段 | 类型 | 说明 |
|---|---|---|
header | FrameHeader | 帧头 |
orientation | Quaternion | 朝向四元数 |
orientation_covariance | list[float] | 朝向协方差(长度 9),首元素为 -1 表示朝向不可用 |
angular_velocity | Vector3F64 | 角速度(rad/s) |
angular_velocity_covariance | list[float] | 角速度协方差(长度 9) |
linear_acceleration | Vector3F64 | 线加速度(m/s²) |
linear_acceleration_covariance | list[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: floatset 接受单个 (kp, kd)(应用到全部关节)或长度 20 的列表(逐关节)。get 返回 20 个元素,离线关节为 None。
JointStateFrame
关节状态帧:hand.joint_states().subscribe().recv() 返回值。变长,仅含在线关节。
| 属性 | 类型 | 说明 |
|---|---|---|
header | FrameHeader | seq / timestamp_us / frame_id |
num_joints | int | 本帧关节数量 |
joints | list[JointStateEntry] | 关节状态数组 |
JointStateEntry
单关节状态。
| 属性 | 类型 | 说明 |
|---|---|---|
nid | int | 节点编号,跨帧定位关节 |
position | float | 位置(rad,关节侧) |
velocity | float | 速度(rad/s) |
effort | float | 力矩 (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_pctStatusWord
状态字:JointDiagnosticsEntry 的 status_word 字段类型,SDK 暴露的公开状态视图。
| 字段 | 类型 | 说明 |
|---|---|---|
ext_state | int | 扩展状态值(如 Init / Ready / Enabled / Stopped) |
ext_state_name | str | ext_state 的语义名 |
position_limit_active | bool | 位置限幅触发 |
velocity_limit_active | bool | 速度限幅触发 |
current_limit_active | bool | 电流限幅触发 |
Hand2CommSummary
帧级通信健康摘要:JointDiagnosticsFrame 的 comm 字段类型,同时覆盖设备内部总线与 SDK 本地端到端(以太网)两段。设备内部快照由 SDK 常驻任务以 1 Hz 自动刷新,无需手动轮询。
| 字段 | 类型 | 说明 |
|---|---|---|
age_ms | int | 设备内部快照距今毫秒数。65535 = 从未成功采样(设备内部字段均为 0)。65534 = 已饱和,快照距今 ≥ 65.5 秒 |
e2e_received | int | 全部订阅流累计收帧数 |
e2e_lost | int | 以太网段经序号缺口检测出的累计丢帧数 |
e2e_reordered | int | 累计乱序/迟到帧数 |
e2e_duplicates | int | 累计重复帧数 |
e2e_window_loss_x100 | int | 最近 1 秒窗口丢包率,0.01% 单位 |
rpc_total | int | 累计请求数。0 表示当前传输后端不提供 RPC 统计(仅 wuji-proto 后端提供,存活的 wuji-proto 连接必然已发过至少一次请求),因此此处为 0 不等于「没有发生重传」 |
rpc_retries | int | 累计请求重传次数 |
rpc_timeouts | int | 累计最终超时的请求数 |
comm_get_failures | int | 累计内部快照刷新失败次数 |
sdk_dropped | int | SDK 内部某一跳落后而丢弃的帧数(累计)——可能是应用侧的订阅消费端,也可能是 SDK 内部的流 handler。与网络丢帧 e2e_lost 语义不同 |
设备内部字段的判读:age_ms 以及每关节的 comm_* 字段同属一份快照,由 SDK 以 1 Hz 刷新。固件升级进行期间会有意跳过这次刷新,因此短暂的陈旧值属预期状态而非故障。用 comm_get_failures 区分两种情况:age_ms 增大而 comm_get_failures 不变 = 正在有意跳过,comm_get_failures 持续增加 = 刷新本身在失败。
计数器饱和,不回绕:本表所有计数达到上限后即停在上限(16 位的 e2e_reordered、e2e_duplicates、rpc_retries、rpc_timeouts、comm_get_failures 停在 65535,32 位的停在 4294967295),不会归零重新计数。1 kHz 流长时间运行时确实可能触顶,因此应按两次采样之间的增量判读,而非当作全生命周期的绝对累计值。
关节编号
| 手指 | S1 | S2 | S3 | S4 |
|---|---|---|---|---|
| thumb | 0 | 1 | 2 | 3 |
| index | 4 | 5 | 6 | 7 |
| middle | 8 | 9 | 10 | 11 |
| ring | 12 | 13 | 14 | 15 |
| pinky | 16 | 17 | 18 | 19 |
标签格式:{finger}_S{1..4}(如 thumb_S1、pinky_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_us | int | 设备时钟偏移量,设备时间 = uptime_us + offset_us |
round_trip_us | int | 本次同步的网络往返时延(微秒),用单调时钟测量,越小越精准 |
synced_at_us | int | 本次同步完成时的主机 UTC 时间戳(微秒) |
SDK 已内置 30 秒后台自动同步,大多数场景无需手动调用 sync_time()。两种情况下手动调用有意义:在时间敏感操作前强制立即同步,或通过返回的 round_trip_us 检查最近一次同步的质量。
时间戳单调性与同步精度
设备侧打戳的 timestamp_us 在所有同步场景下严格单调递增。即使主机时钟回拨,或周期同步算出反向偏移,设备也通过平滑校正避免时间戳回退。每一帧的 timestamp_us 大于上一帧,可直接用于时序排序和采样间隔计算。
同步精度主要由 SDK 与设备之间的网络往返时延决定。TimeSyncResult.round_trip_us 反映这一指标,往返时延越小,偏移量估计越精准。有线以太网直连下,往返时延通常在数百微秒量级。后台周期同步持续校正设备晶振漂移,长时间运行也能保持设备时钟与主机 UTC 对齐。
数据录制
实时采集的关节状态与诊断数据转瞬即逝——录制功能将这些数据持久化到文件,供后续离线分析、算法调试或数据集构建使用。SDK 内置录制引擎,将多通道数据同步写入 MCAP 格式文件。整体流程分三步:
- 创建录制器 — 用
TopicRecorder创建录制器,选择压缩算法 - 注册通道 — 调用
recorder.record(sub)注册要录制的订阅通道 - 开始录制 — 调用
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_frames | int | 总帧数 |
file_size | int | MCAP 文件大小(字节) |
duration_s | float | 录制时长(秒) |
quality | QualitySummary | 质量统计摘要 |
QualitySummary:录制质量汇总统计,RecordingSummary.quality 的类型。
| 字段 | 类型 | 说明 |
|---|---|---|
total_frames | int | 总帧数 |
dropped_frames | int | 丢失帧数 |
frame_drop_rate | float | 丢帧率(0.0~1.0) |
avg_sync_offset_ms | float | 平均同步偏差(毫秒) |
max_sync_offset_ms | float | 最大同步偏差(毫秒) |
sync_rate | float | 同步成功率(0.0~1.0) |
spc_alert_count | int | SPC 告警次数 |
duration_s | float | 录制时长(秒) |
异常处理
所有 SDK 操作错误统一抛 WujiException,消息中携带错误类型前缀(Disconnected、Timeout、PathNotFound、SchemaMismatch、SerializeError、…)。业务侧按需 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() 返回的 severity 与 clear_policy 字段以字符串形式携带同样信息,布局变化时仍然正确。把码值当作不透明标识:记录日志、展示给用户、写进故障报告。解码成功时用返回的 severity 与 clear_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 安装失败
- 确认 Python 版本 ≥ 3.10
- 使用
pip install --upgrade pip更新 pip - 遇到编译错误时,尝试安装预编译的 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。void—wuji_shutdown与资源释放函数。
资源释放函数:
wuji_discovered_free—wuji_scan后释放设备列表wuji_dev_release— 与wuji_connect配套,断开后释放设备句柄wuji_sub_close— 关闭订阅句柄
使用约束(不遵守会出现 use-after-free、死锁或读到失效错误描述):
- typed 回调的 frame 指针仅在回调期间有效,返回后堆字段即被释放。需要保留的数据必须在回调内拷贝出来,不得保存指针。
- 不要在订阅自身的回调里调
wuji_sub_close。close会 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 接口的对应关系
| 功能 | C | Python |
|---|---|---|
| 初始化 | 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_send | hand.joint_command().publish().send([...]) |
| 关节状态订阅 | wuji_hand_2_subscribe_joint_states | hand.joint_states().subscribe() |
| 关节诊断订阅 | wuji_hand_2_subscribe_joint_diagnostics | hand.joint_diagnostics().subscribe() |
| IMU 订阅 | wuji_hand_2_subscribe_imu | hand.imu().subscribe() |
| 错误码描述 | wuji_hand_2_describe_error(code, &info) | WujiHand2.describe_error(code) |
| SDK 用户管理 | wuji_create_user / wuji_switch_user / wuji_list_users | manager.create_user() / switch_user() / list_users() |
| 断开 | wuji_dev_disconnect + wuji_dev_release | device.disconnect() |
CMake 工程示例与最小链接方式见 C 接口参考 · CMake 工程示例。
Beta 1 设备的接口可用性
本页接口在 Beta 1 与 Beta 2 设备上通用,以下能力受硬件条件限制,在 Beta 1 设备上升级固件与 SDK 后仍不可用或行为不同:
| 接口 / 能力 | Beta 1 设备 | 说明 |
|---|---|---|
| 手背状态灯相关行为 | 需相应硬件批次 | 未配备状态灯硬件的 Beta 1 设备无灯效输出,接口调用不报错 |
Hand2CommSummary 的设备内部字段 | 依赖固件版本 | 具体版本门槛暂不提供,将在后续更新 |
完整的硬件与软件对应关系见版本识别与兼容性。