跳至主要内容
版本:3.5.x

08. 远程会话配置器

通过向设备发送 HTTP REST 请求,重新配置已在其他地方另一款应用、Unity 场景或Haply 演示)运行的会话。本教程不会打开 WebSocket:仅通过 GET、POST 和 DELETE 请求来更改基准、工作区预设或安装变换,同时另一款应用仍会继续渲染触觉反馈。

用例

  • 实时调整正在运行的演示。启动Haply Orb演示,然后在另一个终端中运行本教程,以切换基底置换、更改工作空间预设或微调安装变换——Orb 的坐标系会立即发生变化,而无需停止演示。
  • 按用户进行工作区校准。 让触觉场景在主机上继续运行,并让同一网络上的操作员推送一个 mount 进行平移/旋转/缩放,使虚拟工作区与用户的办公桌对齐。
  • 包含设备选择的选项菜单。 相同的 HTTP 辅助函数可以查询 GET /devices (参见 教程 00) 来枚举设备并构建一个交互式菜单——选择设备,然后对其进行重新配置——而无需触及会话的 WebSocket。本教程查询 /sessions 并硬编码 *inverse/0,但切换到一个 /devices-驱动的拾取器是一个本地更改。
  • 脚本化重新配置。在会话开始录制前,自动执行预处理步骤(设置基底 + 预设 + 挂载),而无需在每个客户端中嵌入这些设置。

先决条件

教程 08 将重新配置一个正在运行的会话。你需要一个正在运行的触觉会话——可以是另一个教程、一个 Unity 场景,或者一个Haply 演示。

启动会话的最快方法

打开Haply 并启动Orb演示程序,然后直接将其设为目标:

./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"

“Orb”场景会在设备工作区中渲染一个球体。通过循环切换基底、预设,或使用教程 08 微调安装座的变换,可以实时直观地移动“Orb”的坐标系。

使用方法

# Pick a session interactively (lists every session the service knows)
./08-haply-inverse-http-remote-config
python 08-haply-inverse-http-remote-config.py

# Target the Haply Hub Orb demo directly
./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"

# Target one directly by selector
./08-haply-inverse-http-remote-config --session :my_profile:0
python 08-haply-inverse-http-remote-config.py --session "#42"

# Or by a wildcard profile pattern (first match) — handy when the exact profile is unknown
./08-haply-inverse-http-remote-config --session "co.haply.hub::*:0"

该教程在启动时会打印当前会话的基底/预设/挂载信息,然后等待按键操作——每次按键都会发送一个REST 请求。

在仿真中设置配置文件名称

没有配置文件名称的会话只能通过数字 ID 进行定位——而该 ID 每次运行都会发生变化。请让您的主应用程序调用 session.configure.profile.name 在其第一条消息上,你可以复用一个稳定的选择器,例如 --session :my_profile:0 在每次运行中。参见 会话 — 个人资料名称.

键位设置

关键行动
B循环基置换
P循环工作区预设
W / E / R选择安装编辑模式 — 位置 (mm) / 旋转 (°) / 缩放 (%)
/ 在当前模式下,按 −X / +X 键
/ 在当前模式下,按 +Y / −Y 键
Page Up / Page Down在当前模式下按 +Z / −Z
= / -同时在三个轴上统一缩放比例 ±(始终可用)
DeleteDELETE 基础设置 + 预设 + 挂载 — 恢复为设备默认设置
H显示帮助
Esc退出 (Ctrl+C (也适用)

HTTP 动词 — GET、POST、DELETE

本教程仅使用了三种 HTTP 动词,而且仅此三种。每次调用都会返回标准 JSON 封装 ({"ok": true, "data": {...}} 成功后, {"ok": false, "error": "..."} (发生错误时)以及以下三个状态码之一: 200 成功, 400 请求格式错误, 404 选择器未匹配到任何内容。

动词角色已使用的路径
GET读取当前状态 — 会话列表、目标会话查询、当前配置值/sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=...
POST替换配置值 — 主体为 JSON/<device_selector>/config/{basis,preset,mount}?session=...
DELETE将配置值恢复为设备默认值/<device_selector>/config/{basis,preset,mount}?session=...

HTTP 辅助类

为这三个动词添加一个简单的封装,这样教程的其余部分读起来就像业务逻辑一样:

Python 的用途 requests.Session() 用于 HTTP 保持连接(将每次请求的延迟从约 50 毫秒缩短至约 5 毫秒):

http = requests.Session()

def api_get(path):
r = http.get(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None

def api_post(path, body):
r = http.post(f"{BASE_URL}{path}", json=body, timeout=3)
return r.json() if r.status_code == 200 else None

def api_delete(path):
r = http.delete(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None

def session_url(endpoint):
return f"{endpoint}?session={session_selector}"

会话发现 — GET /sessions

分支位于 --session:

  • --session SELECTOR 给定 → 一 GET /sessions/<SELECTOR>. 200 → 使用它; 404 → 报错。
  • 无国旗GET /sessions (列表) → 渲染带有配置文件名称的会话 → 提示输入索引 → 构建最终选择器(建议 :profile:0 如有可用;否则使用 #id).

SELECTOR 接受在 选择器 — 会话选择器: :profile:instance, #id, :-1, :0、普通个人资料名称,或一个 配置文件名称通配符 类似于……的模式 co.haply.hub::*:0. 该教程将字符串原样转发;服务端对其进行解析。

def discover_session(session_arg):
global session_selector

if session_arg:
# Direct lookup (e.g. ":my_profile:0", "#42", ":-1")
if api_get(f"/sessions/{session_arg}") is None:
return False
session_selector = session_arg
return True

# Otherwise: list and pick
data = api_get("/sessions")
sessions = data.get("data", {}).get("sessions", [])
for i, s in enumerate(sessions):
name = s.get("config", {}).get("profile", {}).get("name", "default")
print(f" [{i}] session #{s['session_id']} profile={name}")

picked = sessions[int(input("Pick session index: "))]
name = picked.get("config", {}).get("profile", {}).get("name", "")
# Prefer the profile selector — it survives restarts; id doesn't
session_selector = (f":{name}:0" if name and name != "default"
else f"#{picked['session_id']}")
return True

设备选择器 — *inverse/0

每个配置调用都与特定设备相关。本教程使用了家族通配符 + 索引选择器:

/*inverse/0/config/<key>
  • *inverse 匹配 Inverse 系列中的任何设备(inverse3, inverse3x, minverse) — 无论具体模型如何,本教程均可照常使用。
  • 0 是该族中从 0 开始的索引——本教程仅涉及第一个 Inverse。

重定向只需修改一行代码:

/verse_grip/0/config/basis?session=... # target first wired VerseGrip
/*verse_grip/*/config/basis?session=... # target every grip, wired + wireless
/inverse3/A14/config/mount?session=... # target Inverse3 with id A14

参见 选择器 — 设备选择器 查看完整的语法。若要构建设备选择器菜单而非硬编码,请使用以下枚举: GET /devices?session=<selector> (教程 00) 并将选定的 device_id 添加到配置路径中。

POST 配置 — 基础、预设、挂载

三个键,请求头结构相同,但请求正文结构不同。每次 POST 请求都会返回一个 200 并将计算结果填入 data404 如果会话/设备选择器未匹配到任何内容。

基础

POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json

{"permutation": "XZY"}

回复: {"ok": true, "data": {"permutation": "XZY"}}

def post_basis():
perm, _ = BASIS_OPTIONS[basis_index]
api_post(session_url("/inverse3/0/config/basis"), {"permutation": perm})

预设

POST /*inverse/0/config/preset?session=:my_profile:0
Content-Type: application/json

{"preset": "arm_front_centered"}

回复: {"ok": true, "data": {"preset": "arm_front_centered"}}

def post_preset():
preset = PRESET_OPTIONS[preset_index]
api_post(session_url("/inverse3/0/config/preset"), {"preset": preset})

安装

POST /*inverse/0/config/mount?session=:my_profile:0
Content-Type: application/json

{
"transform": {
"position": {"x": 0.02, "y": 0.0, "z": 0.0},
"rotation": {"w": 0.966, "x": 0.0, "y": 0.259, "z": 0.0},
"scale": {"x": 1.0, "y": 1.0, "z": 1.0}
}
}

回复: {"ok": true, "data": {"transform": { ... }}} — 反映了归一化后的有效变换。

def post_mount():
body = {
"transform": {
"position": {"x": mount_pos[0], "y": mount_pos[1], "z": mount_pos[2]},
"rotation": quat_from_euler_deg(*mount_rot),
"scale": {"x": mount_scale[0], "y": mount_scale[1], "z": mount_scale[2]},
}
}
api_post(session_url("/inverse3/0/config/mount"), body)
mountpreset 互斥

在设备上发送一个请求会清除另一个请求。本教程并未明确跟踪这一过程——每个 POST 请求都是独立的,由服务器来解决冲突。有关 WebSocket 侧的相同规则,请参阅教程 07

DELETE reset — 三次调用

reset 针对每个配置键触发一次 DELETE 操作。每次操作返回 200 使用其中的默认值 data.

def reset_all():
api_delete(session_url("/inverse3/0/config/basis"))
api_delete(session_url("/inverse3/0/config/preset"))
api_delete(session_url("/inverse3/0/config/mount"))

设置坐骑旋转

transform.rotation 是一个在线上的单位四元数。该教程将旋转存储为 Z-Y-X 内隐欧拉三元组(绕 X 轴俯仰、绕 Z 轴偏航、绕 Y 轴滚转——所有角度),并在每次 POST 时重新组合该四元数。

def quat_from_euler_deg(pitch_x, yaw_z, roll_y):
"""Hamilton quaternion for q = q_z * q_y * q_x (apply X, then Y, then Z)."""
hx, hy, hz = (math.radians(a) * 0.5 for a in (pitch_x, roll_y, yaw_z))
cx, sx = math.cos(hx), math.sin(hx)
cy, sy = math.cos(hy), math.sin(hy)
cz, sz = math.cos(hz), math.sin(hz)
return {
"w": cz*cy*cx + sz*sy*sx,
"x": cz*cy*sx - sz*sy*cx,
"y": cz*sy*cx + sz*cy*sx,
"z": sz*cy*cx - cz*sy*sx,
}
四元数约定

汉密尔顿四元数,右手系,标量在前 (w) — 与该服务的其他部分采用相同的约定,详见 quaternion. 排列顺序是 Z-Y-X 固有 (q = q_z * q_y * q_x): 先绕X轴进行俯仰,然后绕Y轴进行滚转,最后绕Z轴进行偏航。

本教程会在每行状态信息中同时显示推导出的四元数和欧拉三元数,以便您在设备旋转之前验证组合结果。局部欧拉状态从 (0, 0, 0) 无论会话中已有何内容——第一个 mount POST 覆盖原有内容。

输入模型(简要说明)

HTTP 连接才是关键;键盘的用户体验则次之。这里有两个刻意设计的捷径:

  • Python 使用 keyboard 包 — 跨平台,原生支持按住键重复。方向键, Page Up / Page Down= / - 按住键的同时调整坐标轴; BP 按周期运行,并在上升沿触发。
  • C++ 用途 std::getline(std::cin, ...) 以及一种紧凑的标记语法(x+20, sx-5, u+10) — 虽然在需要持续调整时不太符合人体工学,但无需……即可便携 #ifdef- 开发各平台的控制台 API。

来源

随 SDK 安装程序一起发布

教程 08 也会随 SDK 一起安装在本地——请查看 tutorials/08-haply-inverse-http-remote-config/ 位于服务安装目录下。

相关: 会话 — 远程控制·选择器·设备配置·基模式排列·挂载与工作区·JSON 规范·教程 00 — 设备列表·教程 07 — 基模式与挂载(WebSocket 版本)