跳转至主要内容

08. 远程会话配置器

通过向设备发送 HTTP REST 请求,重新配置已在其他地方(例如另一款应用、Unity 场景或Haply Hub演示)运行的会话。 本教程不会打开任何 WebSocket:它仅执行 GET、POST 和 DELETE 请求来修改基础数据、工作区预设或装配变换,而另一款应用程序则继续生成触觉反馈。

用例​

  • 对正在运行的演示进行实时调整。启动演示程序Haply Hub Orb,然后在另一个终端中运行本教程,以更改基本排列、修改工作区预设或微调组装变换——Orb 的坐标系会立即移动,而无需停止演示。
  • 按用户对工作区进行校准。 在主机上保持触觉场景处于活动状态,并允许同一网络中的操作员传输平移、旋转或缩放的 mount 以便虚拟工作区与用户的桌面保持对齐。
  • 设备选择菜单。 相同的 HTTP 实用函数可以查询 GET /devices (参见 教程 00) 用于列出设备并构建一个交互式菜单——选择一个设备,然后对其进行重新配置——而无需触及会话的 WebSocket。本教程将探讨 /sessions 并固定 *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微调编辑变换,都会实时地改变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 调用。

在您的仿真中设置一个配置文件名称

没有配置文件名称的会话只能通过一个数字标识符进行定位,该标识符在每次执行时都会发生变化。请调用 session.configure.profile.name 通过您的主应用程序在收到第一条消息时:届时您就可以复用一个稳定的选择器,例如 --session :my_profile:0 每次执行时。参见 Sessions — 个人资料名称.

键盘快捷键​

说得对行动
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 fourni → 仅一个 GET /sessions/<SELECTOR>. 200 → 使用它; 404 → 报告错误并停止。
  • 无选项 → GET /sessions (列表) → 显示会话及其配置文件名称 → 请求索引 → 构建最终选择器(优先使用 :profile:0 如有;否则使用 #id).

SELECTOR 接受在 选择器 — 会话选择器 : :profile:instance, #id, :-1, :0, 一个简单的个人资料名称,或一个图案 在个人资料名称中包含通配符 comme 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 是该系列中从零开始的索引——本教程仅涉及第一个 Inverse。

更改目标只需修改一行代码:将 --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 已实施 avant 索引 — *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>'. 并以状态结束 1. 不会以过时的默认值启动任何键盘循环。

请查阅 选择器 — 设备选择器 查看完整的语法。若要创建设备选择菜单,而不是硬编码,请使用以下方式列出设备: GET /devices?session=<selector> (教程 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 et 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 et = / - 在被固定期间,会移动安装轴; B et P 在上升沿时滚动浏览basis和preset的数值。
  • 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 — Basis 和 Mount(WebSocket 版本)