Skip to main content

概述

本篇实践介绍如何通过后台回调订阅 Wuji Glove 的实时数据流。应用场景包括实时监控手部追踪及触觉数据、记录采集结果和向上层应用提供反馈。 对应的 Python 示例为 0.subscribe_callback.py,C 示例为 0_subscribe_callback.c。 字段定义见 手部追踪、触觉 和 坐标变换,本页说明集成流程和运行时行为。

前置条件

硬件

本示例只读取,不写任何参数。

软件

前序步骤

无。设备上电、网络连通即可。手部模型标定不是订阅的前提,但会影响 hand_joint_angles 与 hand_skeleton 的贴合度,详见手部模型标定。

数据流

设备流按设备句柄订阅,全局流不带设备句柄、整个进程订阅一次。

帧结构实测

静置手套、无人佩戴、3 秒采样,实测如下。

六条设备流

两条全局流

tf 的输出率比设备流高一个量级,它由多个上游源共同驱动。回调中执行阻塞操作在这条流上最先暴露。

通道数量关系

-1.0 表示该 taxel 无效或被屏蔽,不是压力值。 实测这台手套 744 个通道里有 218 个是 -1.0: 三者收敛到同一个 526。tactile_zones 只收录能映射到手部分区的通道,剩下 172 个未分区通道恰好全是无效的。tactile_point_cloud 则把无效通道整个剔除,只输出有坐标的有效点。 需要「哪些位置在受力」时用点云,需要「哪根手指受力」时用分区,需要原始通道对齐时用 tactile。

骨骼关节名

hand_skeleton 的 21 个关节按 MediaPipe 手部拓扑排列,顺序固定:
joints[0] 是 wrist,位姿恒为原点——整条骨骼链以腕为参考系。腕本身在世界里的位置要看 tf 流的 waist → r_wrist。示例打印 wrist=[+0.000, +0.000, +0.000] 不是数据异常。

关节角的第 5 个分量

hand_joint_angles.fingers[i].angles 长度固定为 5,但只有拇指用满。实测静置状态: 非拇指的第 5 个分量恒为 0,是对齐用的填充位。confidence 反映求解质量,静置未标定时在 0.4 到 0.75 之间浮动。

集成步骤

以下片段按接入顺序展示关键操作,参数解析、信号处理和完整错误分支见示例。

1. 连接目标手套

将 serial_number 替换为目标手套的序列号。连接成功后,后续步骤复用同一个设备句柄。C 片段需包含 wuji_sdk.h。

2. 定义帧处理函数

以触觉流为例,回调只读取本帧数据,并过滤负值表示的无效通道。C 必须先判断帧类型,帧指针只在本次回调内有效。

3. 注册回调并保存订阅句柄

注册后 SDK 在后台线程调用帧处理函数。每条流保存一个订阅句柄,退出时逐一关闭。tf 等全局流通过 manager 或全局 C 接口订阅,整个进程订阅一次即可。

4. 结束订阅并断开连接

主线程保持运行以接收数据。退出或发生异常时,先关闭订阅,再断开设备,避免回调继续访问已释放的资源。

运行与输出

Python 侧六条流交错输出,不限流:
5 秒实测输出 3201 行,约 640 行每秒,与六条流各 130 Hz 相符。 C 侧把打印限流到每 30 帧一次,并多出两条全局流:
以上为实机运行结果,设备是右手 Wuji Glove,手套静置无人佩戴。

注意事项

C 侧回调在专用工作线程上触发,frame 指针只在回调期间有效。堆上的 data、joints、poses 等数组在回调返回后立即释放,不能留存指针。

扩展用法

示例的 avg() 会被无效通道拉成负数

此处为本示例的关键说明。Python 示例用 avg() 算分区均值:
-1.0 直接进了分子,实测掌心 308 个通道里 20 个无效,输出就成了 palm=-0.1——手套根本没受力,读数却是负的。C 示例打 palm[0],如果第一个通道恰好无效就直接显示 -1.000,实测正是如此。 两侧都要先滤掉负值:
滤掉后同一帧的掌心均值是 +0.015。无效通道的比例因手套而异,不要按固定数字写死。

多设备环境指定 SN

两侧示例都遍历扫到的每一只手套逐个连接。共用网络里跑之前用 --sn 限定:
C 示例没有命令行参数,需要在源码里比对 list[i].serial_number 后再连。

只订阅需要的流

六条流全开在静置状态下就是每秒 640 帧回调。做触觉分析只留 tactile 或 tactile_point_cloud,做手部姿态只留 hand_skeleton,回调压力立刻降下来。

避免在回调中执行阻塞操作

每条订阅有独立的工作线程,同一条订阅内的回调串行、保序,不同订阅之间可能并发。跨回调共享的状态要自己加锁。写文件、跑算法、加锁等待都应该挪到消费线程,回调里只做最小拷贝。

换成异步写法

不想用回调可以改 subscribe() 加 recv(),把取帧的节奏握在自己手里,见数据订阅:异步。

相关内容