跳到主要内容

09. WebSocket 工作空间导航器

通过在 WebSocket 通道上流式发送 set_transform 命令,实时导航设备的工作空间位置。本教程使用 HTTP 发现会话和设备(与 教程 08 完全相同),然后打开一个 WebSocket,在每个 tick 推送位置更新。

下表说明了两种变换操作之间的区别:

操作教程典型用法
configure.mount — 持久化的挂载变换08设置一次(或极少更改),用于描述设备的物理安装方式
set_transform — 持久化的工作空间变换09(本教程)同样是持久化的,但可以每个 tick 流式发送,用于实时工作空间导航

两种命令都可通过 HTTP 和 WebSocket 使用,并且都是持久化的——服务会记住你最后发送的值。二者的区别在于用途和更新频率:configure.mount 针对低频更新(物理设置)进行了优化,而 set_transform 针对高频流式发送(场景导航)进行了优化。

当工作空间原点需要跟随运行时发生变化的对象时,请使用 set_transform——例如跟随手部运动、为程序化偏移制作动画,或让操作员实时微调虚拟工作空间。

挂载与工作空间存在父子关系

单独来看,configure.mount 和 set_transform 似乎会产生相同的效果,但二者存在父子关系:在坐标管线中,工作空间变换施加在挂载之上(device → basis → mount → workspace → application)。完整管线参见 挂载与工作空间。

使用场景​

  • 实时工作空间导航。 在不停止触觉场景的情况下,移动设备的光标工作空间(即末端执行器可到达的包络区域)。
  • 操作员驱动的对齐。 让另一个终端中的人按住方向键,在主触觉应用运行时平移工作空间——适用于实时会话期间为每位用户进行桌面校准。
  • 程序化工作空间运动。 通过脚本(音频驱动、物理驱动等)驱动工作空间位置,每个 tick 只发送仅含位置的变换补丁。

前提条件​

教程 09 需要一个正在运行的 WebSocket 会话——即在运行中的触觉循环里已有 Inverse3 的客户端。教程 09 随后会连接到该会话,并实时平移其工作空间。

最快启动会话的方法

打开 Haply Hub,启动 Orb 演示。它会打开一个持久的触觉会话,你可以立即将其作为目标:

./09-haply-inverse-ws-remote-control --session co.haply.hub::demo-orb
python 09-haply-inverse-ws-remote-control.py --session "co.haply.hub::demo-orb"

Orb 场景会在工作空间原点处渲染一个球体——使用教程 09 移动工作空间,会实时平移整个场景,让你即时获得视觉反馈。

任何其他运行触觉循环的教程(01 – 07)同样适用。先在一个终端中启动它,然后在第二个终端中启动教程 09,让它以交互方式发现会话即可。

用法​

# Discover session interactively, use first detected device
./09-haply-inverse-ws-remote-control
python 09-haply-inverse-ws-remote-control.py

# Target the Haply Hub Orb demo directly
./09-haply-inverse-ws-remote-control --session co.haply.hub::demo-orb
python 09-haply-inverse-ws-remote-control.py --session "co.haply.hub::demo-orb"

# Target a session directly, specify device
./09-haply-inverse-ws-remote-control --session :my_profile:0 --device A14
python 09-haply-inverse-ws-remote-control.py --session "#42" --device A14

控制​

按住某个键可持续移动,松开即停止。

按键操作
→ / ←+X / −X
↑ / ↓+Y / −Y
Page Up / Page Down+Z / −Z
= / -提高 / 降低导航速度
0将工作空间重置到原点
H显示帮助
Esc退出(Ctrl+C 同样有效)

工作原理​

第 1 步 — HTTP 会话与设备发现​

本教程复用与 教程 08 相同的 discover_session() / discover_device() 辅助函数:先执行 GET /sessions(若指定了 --session,则为 GET /sessions/<selector>),再执行 GET /devices(或直接使用 --device)。两个辅助函数接受相同的命令行参数,且 SESSION_HELP 文本完全一致——来自教程 00 或 08 的选择器无需修改即可在此使用。

第 2 步 — WebSocket 会话与首条消息握手​

本教程会打开一个指向 ws://localhost:10001 的 WebSocket。仅在收到第一条消息时,它会发送会话配置注册信息:

{
"session": {"configure": {"profile": {"name": "co.haply.inverse.tutorials:ws-remote-control"}}},
"inverse3": [{"device_id": "...", "commands": {"set_transform": {"transform": {"position": {"x": 0, "y": 0, "z": 0}}}}}]
}

后续的 tick 省略 session 字段——仅发送 inverse3 命令数组。

第 3 步 — 每个 tick 的 set_transform​

set_transform 接受部分变换——只需包含你希望修改的字段。本教程仅发送位置;旋转和缩放保持会话的默认值(即 configure.mount 设置的值,如果有的话)。

{
"inverse3": [{"device_id": "...", "commands": {"set_transform": {"transform": {"position": {"x": 0.02, "y": 0.0, "z": 0.01}}}}}]
}

位置会逐 tick 累积。在约 1 kHz 下,服务会在每个 tick 重新应用 set_transform(零阶保持),因此工作空间会保持在你最后一次推送的位置。

set_transform 与 configure.mount 的区别

两个命令都会为每个会话存储一个持久化变换,但在管线中,工作空间变换位于挂载之上(参见 挂载与工作空间)。当教程 09 在每个 tick 流式发送 set_transform 时,每一个新的 WebSocket 帧都会替换之前流式发送的工作空间变换——因此,任何其他向同一设备流式发送 set_transform 的客户端,都会在下一个 tick 被覆盖。configure.mount 相互独立,并保持其值不变。实际使用中,每类职责各选一条通道:挂载用于物理设置,set_transform 用于实时导航。

线程模型(C++)​

C++ 教程使用两个线程共享一个位置累加器:

线程职责
主线程读取 std::getline(std::cin, ...),解析令牌,更新 nav_vel[] 和 default_step
WebSocket 回调线程使用 nav_vel[] * dt 推进 nav_pos[],序列化 set_transform 并发送

std::mutex nav_mutex 保护 nav_pos[]、nav_vel[] 和 default_step。last_tick 时间戳与 first_ws_msg 标志是 WS 回调的局部变量,无需互斥锁。

ws.onmessage = [&](const std::string &) {
float pos_snap[3];
{
std::lock_guard<std::mutex> lk(nav_mutex);
for (int i = 0; i < 3; ++i) nav_pos[i] += nav_vel[i] * 0.001f; // mm/frame → m
for (int i = 0; i < 3; ++i) pos_snap[i] = nav_pos[i];
}
// build and send set_transform with pos_snap
};

源代码​

随 SDK 安装程序一同提供

教程 09 已随 SDK 在本地安装,请在服务安装目录下的 tutorials/09-haply-inverse-ws-remote-control/ 中查找。

相关内容: set_transform 命令 · 会话 — 首条消息握手 · 挂载与工作区 · 选择器 · 教程 08 — HTTP 远程配置 · 教程 07 — Basis 与挂载