주요 내용으로 이동

08. 원격 세션 구성기

다른 곳(예: 다른 앱, Unity 씬 또는 Haply Hub 데모)에서 이미 실행 중인 세션을재구성하려면 해당 장치에 HTTP REST 호출을 전송하면 됩니다. 이 튜토리얼에서는 웹소켓을 열지 않습니다. GET, POST 및 DELETE 요청을 통해 다른 앱에서 햅틱 피드백 렌더링을 계속 진행하면서 basis, 작업 공간 프리셋 또는 마운트 변환을 수정할 수 있습니다.

사용 사례​

  • 실행 중인 데모를 실시간으로 미세 조정할 수 있습니다. Haply Hub Orb 데모를 실행한 다음, 두 번째 터미널에서 이 튜토리얼을 실행하면 basis 축 변위를 전환하거나, 작업 공간 사전 설정을 변경하거나, mount 변환을 미세 조정할 수 있습니다. 데모를 중지할 필요 없이 Orb의 좌표계가 즉시 변경됩니다.
  • 사용자별로 작업 공간 보정을 수행합니다. 콘택트 시나리오가 호스트에서 계속 실행되도록 하고, 동일한 네트워크에 있는 운영자가 푸시할 수 있도록 허용합니다. mount 이동 / 회전 / 확대/축소를 통해 가상 작업 공간을 사용자의 책상 위치에 맞춥니다.
  • 장치 선택 기능이 포함된 옵션 메뉴. 동일한 HTTP 보조 함수를 호출할 수 있습니다 GET /devices(참조: 튜토리얼 00) 장치를 열거하고 대화형 메뉴를 구성합니다. 장치를 선택한 후 해당 장치를 재구성할 수 있으며, 세션 내의 WebSocket을 건드릴 필요가 없습니다. 이 튜토리얼에서는 다음을 조회합니다. /sessions 그리고 하드코딩되어 있으며 *inverse/0,하지만 대신 /devices 드라이버 선택기는 단지 한 곳의 로컬 수정일 뿐입니다.
  • 스크립트를 통한 재구성. 세션 녹화가 시작되기 전에 사전 점검 단계(basis 설정 + 프리셋 + 마운트)를 자동으로 실행하므로, 각 클라이언트에 이러한 설정을 일일이 적용할 필요가 없습니다.

전제 조건​

튜토리얼 08은실행 중인 세션을재구성하는 방법에 관한 내용입니다. 활성 상태인 햅틱 세션(다른 튜토리얼, Unity 씬 또는 ‘ Haply ’ 허브 데모 등)이 하나 필요합니다.

세션을 가장 빠르게 시작하는 방법

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에서 basis, 프리셋 또는 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

튜토리얼은 시작 시 세션의 현재 basis / preset / mount를 출력한 다음, 키 입력을 기다립니다. 키를 누를 때마다 정확히한 번의 REST 호출이 전송됩니다.

시뮬레이션에서 프로필 이름 설정

프로필 이름이 설정되지 않은 세션은 숫자 ID로만 지정할 수 있으며, 이 ID는 실행할 때마다 달라집니다. 메인 앱에서 첫 번째 메시지에서 다음을 호출하도록 하십시오. session.configure.profile.name, 그러면 이후 실행할 때마다 안정적인 선택기를 재사용할 수 있습니다. 예를 들어 --session :my_profile:0. 참조 대화 — 프로필 이름。

키 바인딩​

버튼작업
B순환 전환, basis 치환
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 장기 연결 재사용 구현 (단일 요청 지연 시간을 약 50 ms에서 약 5 ms로 단축 가능):

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 장치만 다룹니다.

대상 장치를 변경하려면 문자열 하나만 수정하면 됩니다 — 전달 --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 인덱스에 있는 클래스그 전발효——“*inverse:left/0”는 “0번째”를 의미합니다.왼손 “inverse”이지, “0번째 inverse, 가급적이면 왼쪽에 있는 것”이 아닙니다. 참조: 선택자 — 클래스 필터는 인덱싱 전에 구문 분석됩니다.。

튜토리얼 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 구성 — basis, preset, mount​

세 개의 키에 대한 요청 구조는 동일하지만, 요청 본문의 구조는 다릅니다. 각 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를 실행하면 장치에서 다른 하나가 삭제됩니다. 이 튜토리얼에서는 이 점을 명시적으로 다루지 않습니다. 각 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 내회전 오일러 각 3원조(X축을 중심으로 한 피치(pitch), Z축을 중심으로 한 요(yaw), Y축을 중심으로 한 롤(roll), 모두 도 단위)로 저장하고, 매 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,
}
사원수 규칙

단위 해밀턴 4원수, 오른손 좌표계, 스칼라가 앞에 온다 (w)——본 서비스의 다른 부분에서 정한 약관과 동일하며, 다음을 참조하십시오. quaternion. 조합 순서는 다음과 같습니다. Z-Y-X 내회전 (intrinsic)(q = q_z * q_y * q_x):먼저 X축을 중심으로 피치(pitch)를 적용하고, 다음으로 Y축을 중심으로 롤(roll)을 적용한 뒤, 마지막으로 Z축을 중심으로 요(yaw)를 적용합니다.

이 튜토리얼에서는 각 상태 행마다 도출된 4원수와 오일러 각도 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 버전)