跳转至主要内容

08. 远程会话配置器

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

应用场景​

  • 实时调整正在运行的演示。在Haply Hub中启动Orb演示,然后在第二个终端中执行本教程,以交换基本置换、更改工作区预设或轻微移动挂载变换——Orb场景的坐标系会立即发生变化,而无需停止演示。
  • 每位用户的工位校准。 让触觉场景在主计算机上运行,并允许同一网络中的操作员进行 mount- 传输偏移量/旋转角度/缩放比例,以便虚拟工作区与用户的桌面对齐。
  • 包含设备选择的选项菜单。 这些相同的 HTTP 辅助函数可以 GET /devices 查询(参见 教程 00),用于列出设备并构建交互式菜单——选择一个设备,然后对其进行重新配置——而无需触及会话的 WebSocket。本教程询问 /sessions ab 且硬编码 *inverse/0; 转向一个…… /devices 然而,受控选择是一种局部更改。
  • 基于脚本的重新配置。在录制会话开始前,自动执行前期步骤(设置基础配置、预设和挂载),而无需在每个客户端中嵌入这些设置。

先决条件​

教程 08 用于重新配置一个正在运行的会话。您需要一个任意正在运行的触觉会话——可以是另一个教程、Unity 中的场景,或是Haply Hub 中的演示。

启动会话的最快方法

打开Haply Hub,启动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“基础”和“预设”之间切换,或者稍微移动“Mount”变换,则“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"

# Pick a specific device on the session (default is *inverse/0)
./08-haply-inverse-http-remote-config --device 'inverse3/#A12' # by exact id
./08-haply-inverse-http-remote-config --device '*inverse:left/0' # first left-handed inverse
./08-haply-inverse-http-remote-config --device '*inverse:right/0' # first right-handed inverse
./08-haply-inverse-http-remote-config --device '*verse_grip/0' # first grip-family device

该教程在启动时会显示当前会话的基础设置/预设/挂载点,然后等待按键操作——每次按键都会发送一个REST调用。

在您的仿真中设置一个轮廓名称

没有配置文件名称的会话只能通过数字 ID 来引用——而且该 ID 在每次运行时都会发生变化。请让您的主应用程序在发送第一条消息时 session.configure.profile.name 调用;之后,您可以使用一个稳定的选择器,例如 --session :my_profile:0 在所有运行中重复使用。参见 会话 — 用户名.

按键映射​

口味Aktion
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-Envelope ({"ok": true, "data": {...}} 如果成功的话, {"ok": false, "error": "..."} (出现错误时)以及以下三个状态码之一: 200 Erfolg, 400 请求有误, 404 Selektor 没有找到任何结果。

MethodeZweck使用的路径
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 Keep-Alive(将每次请求的延迟从 ~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 已指定 → ein GET /sessions/<SELECTOR>. 200 → 使用; 404 → 显示错误。
  • 无国旗 → GET /sessions (列表) → 显示带有配置文件名称的会话 → 查询索引 → 生成最终选择器(优先 :profile:0,如有;否则 #id).

SELECTOR 接受以下任何一种形式: Selektoren — 会议选择器 定义如下: :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 在此系列中采用的是从零开始的索引——本教程始终仅讨论第一个逆函数。

切换到另一台设备只需修改一个字符串——请传递 --device (路径由此生成):

/verse_grip/0/config/basis?session=... # --device verse_grip/0
/*verse_grip/0/config/basis?session=... # --device '*verse_grip/0' — first grip, any kind
/inverse3/A14/config/mount?session=... # --device 'inverse3/#A14'
/*inverse:left/0/config/basis?session=... # --device '*inverse:left/0' — first left-handed inverse
/*inverse:right/0/config/basis?session=... # --device '*inverse:right/0' — first right-handed inverse

班 :left / :right 将 vor 在索引过程中应用—— *inverse:left/0 意为“第0个 左撇子 “逆元”,而不是“第0个逆元,希望是左逆元”。参见 选择器——类过滤器将在索引生成之前被解析.

如果未检测到所提及的设备,教程 08 将结束

该教程在启动时会显示一个 GET …/config/basis-请求。如果会话中没有 设备,则 --device 相当于(例如 --device '*inverse:left/0' 在 仅支持右侧布局的情况下,该服务会返回 404 no device matches selector '<sel>'; 该教程提供了 Device '<sel>' not found in session '<sel>'. aus 并以该状态结束 1. 此时不会启动使用过时默认值的键盘循环。

完整的语法请参见 Selektoren — 设备选择器. 若要创建一个设备选择菜单,而不是使用硬编码的值,请统计设备数量,方法是 GET /devices?session=<selector> auf (教程 00) 并应用所选的 device_id 到配置路径中。

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

三个请求结构相同但正文模式不同的请求。每个 POST 请求都会返回一个 200 将所得值代入 data,或者一个 404,如果会议或设备选择器未找到匹配项。

基础​

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)
mount und preset 相互排斥

对这两个字段中的任意一个发送 POST 请求,都会在设备上删除另一个字段中的内容。本教程并未明确说明这一点——每个 POST 请求都是独立的,由服务器来解决冲突。关于从 WebSocket 角度看同一规则的说明,请参阅教程 07。

DELETE-重置 — 三次调用​

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 以及 = / - 在按住该按键期间,移动安装轴; B und P 在上升沿时,在“基础”和“预设”之间循环切换。
  • C++ verwendet 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 版本)