Wuji Hand 2 指尖触觉

Wuji Hand 2 通过五路按指数据流提供指尖触觉数据。本文集中说明硬件与版本要求、自描述数据格式、Python 与 C 接口、零基线标定、状态查询、通信诊断和完整示例。

使用要求

指尖触觉需要配备指尖触觉传感器的 Wuji Hand 2.2 (Beta) 硬件,并配套使用固件 v2.1.0 与 Wuji SDK v2026.7.21 或更高版本。

手指触点数量数据流
拇指40fingertip/thumb/data
食指34fingertip/index/data
中指34fingertip/middle/data
无名指34fingertip/ring/data
小指34fingertip/pinky/data

各数据流原生输出率为 100 Hz。订阅后可通过 Subscription.set_rate() 调低输出率,传入 0 恢复设备默认值。C SDK 使用 wuji_sub_set_rate()。调节输出率需要 Wuji Hand 2 固件 v2.3.0 与 Wuji SDK v2026.8.17 或更高版本。

接口概览

操作PythonC
读取传感器元数据hand.fingertip_<finger>_info().get()wuji_hand_2_get_fingertip_<finger>_info()
订阅指尖数据hand.fingertip_<finger>_data().subscribe()wuji_hand_2_subscribe_fingertip_<finger>_data()
整手零基线标定hand.tactile_calibrate()wuji_hand_2_tactile_calibrate()
查询单指状态hand.tactile_status(finger)wuji_hand_2_tactile_status()
调低数据流输出率sub.set_rate(frequency_hz)wuji_sub_set_rate()

Python 的 fingertip_<finger>_info()fingertip_<finger>_data() 在方法名里指定手指,<finger>thumbindexmiddleringpinkytactile_status() 使用 "thumb""index""middle""ring""pinky"。C 的 wuji_hand_2_tactile_status()finger 参数 04 依次表示拇指、食指、中指、无名指和小指。

自描述数据格式

先读取该手指的 FingertipSensorInfo,再按其 format JSON 解码 FingertipSensorData.data。不要固定数据长度、字段偏移、比例或单位。

import json

from wuji_sdk import WujiException

try:
    info = hand.fingertip_thumb_info().get()
except WujiException as exc:
    # 上电后立即读取、未安装传感器或传感器固件较早时属于预期情况。
    # 跳过该手指,继续解码其余手指。
    print(f"thumb: no sensor info yet ({exc})")
else:
    fmt = json.loads(info.format)
    sub = hand.fingertip_thumb_data().subscribe()
    frame = await sub.recv_async()
    if frame.info_digest != info.digest:
        info = hand.fingertip_thumb_info().get()
        fmt = json.loads(info.format)
        # 按更新后的 fmt 重建解码器

手指可能暂时没有元数据:未安装传感器、传感器固件较早,或上电后立即读取时手部仍在收集。此时读取元数据会抛出 WujiException。跳过该手指并继续解码其余手指即可。这是预期状态,不是故障。

元数据与数据帧

类型字段说明
FingertipSensorInfoheader帧序号、时间戳和坐标系
FingertipSensorInfodigest元数据内容摘要,用于绑定数据帧
FingertipSensorInfomodel传感器型号字符串,可能为空
FingertipSensorInfodevice_type传感器类型
FingertipSensorInforate_hz原生输出率,当前为 100 Hz
FingertipSensorInfoformat描述数据布局与安装位置的 JSON
FingertipSensorDataheader数据帧序号、时间戳和坐标系
FingertipSensorDatainfo_digest生成该数据帧时使用的元数据摘要
FingertipSensorDatadataformat 解码的原始字节。Python 以 list[int] 交付,调用 struct.unpack_from() 前先用 bytes(frame.data) 转换。C 以 uint8_t *datadata_len 交付

format 中的 vencodingpoint_countpoint_stridepoint_fieldsaggregate_strideaggregate_fieldsunit 描述数据布局。解码前先确认 v1encodingpoint_array。数据长度等于 point_count * point_stride + aggregate_stride,可用于校验每一帧。每个触点包含 fxfyfz。聚合字段提供三轴合力与温度。

固件 v2.4.0 及更高版本按传感器满量程归一化上报分点力。法向轴范围为 0 到 1,两个切向轴范围为 -1 到 1,超量程截断。更早的固件以牛顿上报分点力。三轴合力保持以牛顿为单位,温度保持以摄氏度为单位。读取 format 中的 scaleunit 可兼容两种格式。归一化格式需要 Wuji SDK v2026.8.17 或更高版本。

触点在手模型中的位置

固件 v2.5.0 及更高版本会在 format 中提供安装位姿字段 point_rpybase_xyzbase_rpyformat 可以只包含 positions,而不包含其他安装位姿字段。仅当 point_rpybase_xyzbase_rpy 全部存在时,才表示固件提供完整的安装位姿。positionspoint_rpy 都必须包含 point_count 行数据,每行包含 3 个数字。base_xyzbase_rpy 都必须包含 3 个数字。如果只存在部分字段或数组形状不匹配,将格式视为无效,不要使用不完整的元数据放置触点。

positionspoint_rpy 位于传感器模块坐标系,base_xyzbase_rpy 描述该坐标系相对手指 tip_sensor_frame 的位姿。按以下公式换算每个触点及其受力坐标轴:

point_link = R(base_rpy) * positions[i] + base_xyz
force_link = R(base_rpy) * R(point_rpy[i]) * [fx, fy, fz]

R(rpy) 按 URDF 固定轴顺序使用 roll、pitch 和 yaw:Rz(yaw) * Ry(pitch) * Rx(roll)format 上报的位置单位为米,角度单位为弧度。两个公式都将传感器模块坐标系中的数值换算到手指 tip_sensor_frame

设备上报与左右手类型匹配的安装位姿,因此左手和右手可使用相同的坐标变换。如果 3 个安装位姿字段都不存在,仍可读取力数据,但无法将触点放置到手模型中。

定位合力作用点

固件 v2.6.0 及更高版本会在 format 中提供 aggregate_xyzaggregate_rpyaggregate_xyz 是传感器垫在传感器模块坐标系中的固定几何质心,aggregate_fields 中的合力作用于该点。该点不随每帧的压力中心变化。按以下公式换算到手指连杆坐标系:

point_link = R(base_rpy) * aggregate_xyz + base_xyz

aggregate_rpy 描述合力三轴在同一传感器模块坐标系中的姿态。按以下公式换算合力及其相对手指连杆原点的力矩:

force_link = R(base_rpy) * R(aggregate_rpy) * [fx, fy, fz]
moment_link = point_link × force_link

由于 aggregate_xyz 固定不变,moment_link 是合力在固定力臂下的力矩,不是实测的接触力矩。

v2.6.0 之前的固件仍会上报合力,但不提供作用点和力矩。缺少 aggregate_rpy 时,无论是否存在 aggregate_xyz,都只用传感器安装姿态 R(base_rpy) 表示合力三轴。如果 aggregate_xyzaggregate_rpy 存在,但值不是 3 个数,就把整个 format 视为无效,不要当作该字段缺失。

读取与显示示例

Python 与 C 示例读取五指元数据,构建与当前 digest 匹配的解码器。Python 只订阅成功读到元数据的手指,C 订阅五路数据流并在显示时跳过没有元数据的手指。终端以 100 Hz 刷新同一显示区域,展示以下信息:

  • 每个手指的合力、温度、接触点数量和受力最大的触点
  • 每个触点的三轴力,其中 fxfy 表示局部 x、y 轴切向力,fz 表示局部 z 轴法向力
  • 受力最大的触点在手模型中的位置与力方向(需要固件 v2.5.0 及更高版本)
  • 作用点位置及合力相对手指连杆原点产生的力矩(需要固件 v2.6.0 及更高版本)

帧处理与状态

Python 示例在每次刷新时清空各路订阅队列,仅显示每个手指收到的最新帧。C 示例保存每个手指的最新帧,再由主线程统一刷新显示。info_digest 与当前元数据不一致时,对应状态行会提示重新读取 FingertipSensorInfo 并重建解码器。数据长度与 format 不一致时,示例跳过该手指的当前帧,其他手指继续更新。

C 示例在每个手指状态行显示等待数据、数据滞后、数据流结束、数据流错误、元数据变化和数据长度异常。Python 示例遇到单指数据长度异常时也会继续运行。

格式校验

两个示例都要求 aggregate_fields 包含 fxfyfztemperature。C 示例在订阅前校验点数、stride、字段偏移和比例值,并拒绝非有限值、非整数布局值、越界字段和超过 2048 字节的单指数据布局。

力值超出预设列宽时,表格会扩展该行,不会截断数值。

示例按 format 声明的单位选择接触阈值。归一化数据使用 0.02,牛顿数据使用 0.2 N。这些数值用于示例显示,不属于 SDK 数据契约。

C 订阅回调在 SDK 工作线程上运行。回调收到的帧及其 data 只在本次回调期间有效。示例在回调返回前将数据复制到互斥锁保护的缓冲区,再由主线程渲染五指快照。通过示例提供的 CMakeLists.txt 构建,构建配置会链接数学库和 Threads/pthread。结束运行时,通过 wuji_sub_close() 关闭订阅。

零基线标定与状态查询

标定前保持五个传感器表面无负载。一次调用会按拇指到小指的顺序触发全部传感器。使用固件 v2.6.0 与 Wuji SDK v2026.8.31 或更高版本时,遇到离线手指会立即抛出 NodeOfflineError,后续手指不会继续触发。C SDK 对该手指返回 WUJI_STATUS_ERR_NODE_OFFLINE。更早版本抛出 WujiException

import time

from wuji_sdk import NodeOfflineError, TactileState

hand.tactile_calibrate()

pending = ["thumb", "index", "middle", "ring", "pinky"]
deadline = time.monotonic() + 30.0
while pending and time.monotonic() < deadline:
    not_ready = []
    for finger in pending:
        try:
            if hand.tactile_status(finger).state != TactileState.Ready:
                not_ready.append(finger)
        except NodeOfflineError:
            # 该手指的触觉模块离线,继续等待
            not_ready.append(finger)
    pending = not_ready
    if pending:
        time.sleep(0.5)

if pending:
    raise RuntimeError(f"tactile calibration timed out: {pending}")
print("all fingertip sensors are ready")

调用成功表示标定命令已发送到全部手指,不表示标定已经完成。轮询五个手指的状态,直到全部为 TactileState.Ready,并为整个等待过程设置总超时。单次状态查询为阻塞调用,最长约 1 秒,五指轮询一轮约需 5 秒,超时只在两轮之间检查。手指的触觉模块离线时,tactile_status() 抛出 NodeOfflineError,版本要求同上。示例把该手指视为未就绪并继续等待,直到超时。

TactileType.Thumb 表示 40 触点拇指传感器,TactileType.Standard 表示 34 触点标准传感器。运行状态包括 TactileState.CalibratingTactileState.Ready

处理传感器离线

传感器离线时仍可创建对应的指尖数据订阅。如果订阅未收到数据帧,调用对应手指的 tactile_status,确认传感器是否离线。

使用固件 v2.6.0 与 Wuji SDK v2026.8.31 或更高版本时:

  • Python 的 fingertip_<finger>_info().get()tactile_statustactile_calibrate 会抛出 NodeOfflineError
  • 对应的 C 接口会返回 WUJI_STATUS_ERR_NODE_OFFLINE (-12)
  • 通用 GET、SET 和 EXEC 请求的目标离线时,也会返回相同类型的结果。

不支持节点离线错误的固件或 SDK 版本会报告通用内部错误。NodeOfflineError 继承自 WujiException,不继承 RuntimeError。已捕获 WujiException 的代码无需调整。为这些操作捕获 RuntimeError 的代码需改为捕获 NodeOfflineErrorWujiException

from wuji_sdk import NodeOfflineError, WujiException

try:
    hand.tactile_status("ring")
except NodeOfflineError:
    print("无名指指尖传感器离线")
except WujiException as error:
    print(f"SDK 错误:{error}")

该结果只表示设备无法与传感器通信,不能据此判断具体原因。先确认模组连接。如果设备上电后传感器仍离线,请联系技术支持,模组可能存在硬件损伤。重复执行相同请求无法恢复通信。

触觉通信诊断

通过 hand.joint_diagnostics().subscribe() 订阅后,从 recv()recv_async() 返回的每一帧读取 JointDiagnosticsFrame.comm

sub = hand.joint_diagnostics().subscribe()
frame = await sub.recv_async()
comm = frame.comm

comm 包含触觉通信状态:

字段说明
age_ms设备内部快照距今的毫秒数。65535 表示从未成功采样,65534 表示快照已有 65.5 秒或更久
comm_get_failures快照刷新失败的累计次数
tactile_online_mask五指触觉在线位图,bit 0 到 bit 4 依次表示拇指到小指
tactile_response_rate_pct5 元数组,每个手指的总线响应率,范围为 0 到 100。索引 0 到 4 依次表示拇指到小指,与 tactile_online_mask 位序一致
tactile_timeout_total5 元数组,每个手指累计总线超时次数,索引顺序相同。计数达到 4294967295 后饱和不回绕,应按两次采样的差值判读

先用 tactile_online_mask 定位离线手指,再结合响应率和超时次数判断通信是否持续退化。

位图与两个按指数组都来自设备内部快照。快照不可用时,三者均为 0。把全 0 位图判定为五指离线之前,先检查 age_msage_ms 持续增长而 comm_get_failures 不变,表示 SDK 有意跳过刷新,触觉标定和固件升级期间都会如此。age_ms 增长且 comm_get_failures 同步增加,表示刷新本身失败。

Hand2CommSummary 完整字段见 Wuji Hand 2 SDK 接口参考 — Hand2CommSummary

订阅更新