주요 콘텐츠로 건너뛰기
버전: 3.5.x

08. 원격 세션 구성 도구

다른 앱, Unity 씬, Haply 데모 등 다른 곳에서 이미 실행 중인 세션을 해당 기기에 HTTP REST 호출을 전송하여 재구성할 수 있습니다. 이 튜토리얼에서는 WebSocket을 열지 않으며, 다른 앱이 햅틱 효과를 계속 렌더링하는 동안 GET, POST, DELETE 요청을 통해 베이스, 워크스페이스 프리셋 또는 마운트 트랜스폼을 변경합니다.

사용 사례

  • 실행 중인 데모를 실시간으로 조정해 보세요. Haply Orb 데모를 시작한 다음, 별도의 터미널에서 이 튜토리얼을 실행하여 기저 순열을 바꾸거나, 작업 공간 사전 설정을 변경하거나, 장착 변환을 미세 조정해 보세요. 데모를 중지하지 않아도 Orb의 좌표계가 즉시 변경됩니다.
  • 사용자별 작업 공간 보정. 메인 컴퓨터에서 햅틱 시뮬레이션을 계속 실행한 상태에서, 같은 네트워크에 있는 운영자가 mount 가상 작업 공간이 사용자의 책상과 일치하도록 오프셋/회전/크기 조정을 수행합니다.
  • 기기 선택이 포함된 옵션 메뉴. 동일한 HTTP 헬퍼를 사용하여 쿼리를 수행할 수 있습니다 GET /devices (참조: 튜토리얼 00)를 사용하여 장치를 열거하고 대화형 메뉴를 구축합니다. 이 메뉴에서는 세션의 WebSocket을 건드리지 않고도 장치를 선택한 다음 재구성할 수 있습니다. 이 튜토리얼에서는 /sessions 그리고 하드코딩하고 *inverse/0, 하지만 ~로 바꾸면 /devices-driven 피커는 로컬 변경 사항입니다.
  • 스크립트를 통한 재구성. 세션 녹화가 시작되기 전의 사전 준비 단계(베이스 설정 + 프리셋 적용 + 마운트)를 자동화하여, 모든 클라이언트에 해당 설정을 일일이 적용할 필요가 없습니다.

전제 조건

튜토리얼 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’ 장면은 장치 작업 공간에 구체를 렌더링합니다. ‘Basis’나 ‘Preset’을 순환하거나 튜토리얼 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 헬퍼

세 개의 동사를 감싸는 간단한 셸을 만들어, 튜토리얼의 나머지 부분은 비즈니스 로직처럼 읽히도록 합니다:

파이썬의 용도 requests.Session() HTTP 키프-얼라이브(Keep-Alive)를 위해 (요청당 지연 시간을 약 50ms에서 약 5ms로 단축):

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을 기점으로 하는 인덱스입니다. 튜토리얼에서는 첫 번째 역함수만 다룹니다.

리타게팅은 한 줄의 코드 변경만으로 가능합니다:

/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 결과 값을 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 그리고 preset 서로 배타적이다

하나를 게시하면 기기에서 다른 하나가 지워집니다. 이 튜토리얼에서는 이를 명시적으로 다루지 않습니다. 각 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 내재적 오일러 3원조(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축을 중심으로 요를 적용합니다.

이 튜토리얼은 기기가 회전하기 전에 합성 결과를 확인할 수 있도록, 모든 상태 줄에 유도된 쿼터니언과 오일러 3원수를 함께 출력합니다. 로컬 오일러 상태는 (0, 0, 0) 세션에 이미 무엇이 있든 간에 — 첫 번째 mount POST 기존 내용을 덮어씁니다.

입력 모델 (개요)

HTTP 연결이 핵심이며, 키보드 사용자 경험은 부차적인 문제입니다. 의도적으로 선택한 두 가지 단축 방법:

  • 파이썬 ~를 사용합니다 keyboard 패키지 — 크로스 플랫폼 지원, 키 누르기 유지 반복 기능을 기본적으로 지원합니다. 방향키, Page Up / Page Down= / - 누르고 있는 동안 마운트 축을 단계별로 이동합니다; B 그리고 P 사이클 단위로, 상승 에지에서 사전 설정됩니다.
  • 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 버전)