08. 원격 세션 구성기
다른 곳에서 이미 실행 중인 세션(다른 앱, Unity 씬, Haply Hub 데모 등)을 재구성합니다. 이 튜토리얼은 WebSocket를 열지 않으며, 디바이스에 HTTP REST 호출(GET, POST, DELETE)만 보냅니다. 다른 앱이 햅틱 렌더링을 계속하는 동안 basis, 워크스페이스 프리셋, 마운트 transform을 변경할 수 있습니다.
사용 사례
- 실행 중인 데모 실시간 조정. Haply Hub Orb 데모를 시작한 다음, 두 번째 터미널에서 이 튜토리얼을 실행하여 basis 순열을 바꾸거나 워크스페이스 프리셋을 변경하거나 마운트 transform을 미세 조정합니다. 데모를 멈추지 않아도 Orb의 좌표 프레임이 즉시 전환됩니다.
- 사용자별 워크스페이스 캘리브레이션. 주 머신에서 햅틱 씬을 계속 실행하고, 같은 네트워크의 운영자가
mount오프셋 / 회전 / 스케일을 전송하도록 합니다. 그러면 가상 워크스페이스가 사용자의 책상과 맞춰집니다. - 디바이스 선택 옵션 메뉴. 같은 HTTP 헬퍼를 사용해
GET /devices(튜토리얼 00 참고)를 질의하면 디바이스 목록을 열거하고 대화형 메뉴를 만들 수 있습니다. 디바이스를 선택한 뒤 재구성하며, 세션의 WebSocket는 건드리지 않습니다. 이 튜토리얼은/sessions를 질의하고*inverse/0을 하드코딩하지만,/devices기반 선택기로 전환하는 것은 로컬 변경으로 가능합니다. - 스크립트 기반 재구성. 세션 녹화가 시작되기 전에 사전 단계(basis + preset + mount 설정)를 자동화합니다. 설정을 모든 클라이언트에 포함시킬 필요가 없습니다.
사전 요구 사항
튜토리얼 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 씬은 디바이스 워크스페이스에 구(sphere)를 렌더링합니다. 튜토리얼 08로 basis나 프리셋을 순환 변경하거나 마운트 transform을 미세 조정하면 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과 같은 고정 셀렉터를 재사용할 수 있습니다. 세션 — 프로필 이름을 참조하십시오.
키 바인딩
- Python
- C++
| 키 | 동작 |
|---|---|
B | basis 순열 순환 |
P | 워크스페이스 프리셋 순환 |
W / E / R | 마운트 편집 모드 선택 — 위치 (mm) / 회전 (°) / 배율 (%) |
← / → | 현재 모드에서 −X / +X 방향으로 한 단계 이동 |
↑ / ↓ | 현재 모드에서 +Y / −Y 방향으로 한 단계 이동 |
Page Up / Page Down | 현재 모드에서 +Z / −Z 방향으로 한 단계 이동 |
= / - | 세 축을 동시에 균일하게 ± 배율 조정 (항상 사용 가능) |
Delete | basis + preset + mount에 DELETE 적용 — 장치 기본값으로 되돌리기 |
H | 도움말 표시 |
Esc | 종료 (Ctrl+C도 동작) |
줄 단위 입력 — 명령을 입력한 뒤 ENTER를 누릅니다.
| 명령 | 동작 |
|---|---|
b | basis 순열 순환 |
p | 워크스페이스 프리셋 순환 |
w / e / r | 마운트 편집 모드 선택 — 위치 (mm) / 회전 (°) / 배율 (%) |
x+[N] … z-[N] | 현재 축을 활성 모드의 기본 단위로 N만큼 이동 (x+만 입력하면 기본값 5) |
sx+[N] … sz-[N] | 한 축에 대한 비균일 배율 단축 명령 (백분율), 항상 사용 가능 |
u+[N] / u-[N] | 세 축을 동시에 균일하게 ± N % 배율 조정 |
reset | basis + preset + mount에 DELETE 적용 |
h | 도움말 표시 |
Ctrl+C (또는 Ctrl+D / EOF)를 누르면 종료합니다.
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
- C++ (nlohmann)
- C++ (Glaze)
Python는 HTTP keep-alive를 위해 requests.Session()을 사용합니다 (요청당 지연 시간을 약 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}"
libhv는 requests::get / requests::post / requests::Delete를 제공합니다 (delete는 C++ 키워드이므로 대문자 D를 사용합니다). POST에서 Content-Type: application/json을 설정하려면 요청을 직접 구성해야 합니다:
static std::string session_url(const std::string &endpoint) {
return BASE_URL + endpoint + "?session=" + session_selector;
}
static json http_get(const std::string &url) {
auto resp = requests::get(url.c_str());
if (!resp || resp->status_code != 200) return {};
try { return json::parse(resp->body); } catch (...) { return {}; }
}
static bool http_post_json(const std::string &url, const json &body) {
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = url;
req->content_type = APPLICATION_JSON;
req->body = body.dump();
auto resp = requests::request(req);
return resp && resp->status_code == 200;
}
static bool http_delete(const std::string &url) {
auto resp = requests::Delete(url.c_str());
return resp && resp->status_code == 200;
}
응답 본문은 항상 {"ok", "data": T} 엔벨로프를 갖습니다. 단일 템플릿이 타입이 지정된 모든 GET 요청을 감싸며, 같은 HttpRequest 패턴을 glz::write_json과 함께 사용하면 POST 요청도 처리할 수 있습니다:
template <typename T> struct envelope { bool ok{}; T data{}; };
template <typename Payload>
static std::optional<Payload> http_get_envelope(const std::string &url) {
auto resp = requests::get(url.c_str());
if (!resp || resp->status_code != 200) return std::nullopt;
envelope<Payload> env{};
if (glz::read<glz_settings>(env, resp->body)) return std::nullopt;
return std::move(env.data);
}
template <typename Body>
static bool http_post_json(const std::string &url, const Body &body) {
std::string buf;
if (glz::write_json(body, buf)) return false;
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = url;
req->content_type = APPLICATION_JSON;
req->body = std::move(buf);
auto resp = requests::request(req);
return resp && resp->status_code == 200;
}
static bool http_delete(const std::string &url) {
auto resp = requests::Delete(url.c_str());
return resp && resp->status_code == 200;
}
세션 탐색 — 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과 같은 프로필 이름 와일드카드 패턴입니다. 튜토리얼은 이 문자열을 그대로 전달하며, 파싱은 서비스가 담당합니다.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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
static bool discover_session(const std::string &session_arg) {
if (!session_arg.empty()) {
const auto data = http_get(BASE_URL + "/sessions/" + session_arg);
if (data.is_null()) return false;
session_selector = session_arg;
return true;
}
const auto data = http_get(BASE_URL + "/sessions");
const json &list = data["data"]["sessions"];
for (size_t i = 0; i < list.size(); ++i) {
const int sid = list[i].value("session_id", 0);
std::string prof = "default";
if (list[i].contains("config") && list[i]["config"].contains("profile"))
prof = list[i]["config"]["profile"].value("name", std::string{"default"});
printf(" [%zu] session #%d profile=%s\n", i, sid, prof.c_str());
}
std::string line; std::getline(std::cin, line);
const json &picked = list[std::stoi(line)];
std::string prof;
if (picked.contains("config") && picked["config"].contains("profile"))
prof = picked["config"]["profile"].value("name", std::string{});
session_selector = (!prof.empty() && prof != "default")
? ":" + prof + ":0"
: "#" + std::to_string(picked.value("session_id", 0));
return true;
}
응답 형태를 구조체로 모델링하면, Glaze가 이를 자동으로 리플렉션합니다:
struct profile_info { std::string name; };
struct session_config{ std::optional<profile_info> profile; };
struct session_info { int session_id{}; std::optional<session_config> config; };
struct sessions_list { int session_count{}; std::vector<session_info> sessions; };
static bool discover_session(const std::string &session_arg) {
if (!session_arg.empty()) {
auto resp = requests::get((BASE_URL + "/sessions/" + session_arg).c_str());
if (!resp || resp->status_code != 200) return false;
session_selector = session_arg;
return true;
}
auto list = http_get_envelope<sessions_list>(BASE_URL + "/sessions");
if (!list || list->sessions.empty()) return false;
for (size_t i = 0; i < list->sessions.size(); ++i) {
const auto &s = list->sessions[i];
std::string prof = "default";
if (s.config && s.config->profile) prof = s.config->profile->name;
printf(" [%zu] session #%d profile=%s\n", i, s.session_id, prof.c_str());
}
std::string line; std::getline(std::cin, line);
const auto &picked = list->sessions[std::atoi(line.c_str())];
std::string prof;
if (picked.config && picked.config->profile) prof = picked.config->profile->name;
session_selector = (!prof.empty() && prof != "default")
? ":" + prof + ":0"
: "#" + std::to_string(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이고, 아마 왼쪽인 것”이 아닙니다. 자세한 내용은 선택자 — 인덱싱 이전에 클래스 필터 해석을 참조하십시오.
튜토리얼은 시작 시 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 요청은 결과 값을 data에 담아 200을 반환하며, 세션 또는 장치 선택자가 일치하는 항목이 없으면 404를 반환합니다.
Basis
POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json
{"permutation": "XZY"}
응답: {"ok": true, "data": {"permutation": "XZY"}}
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_basis():
perm, _ = BASIS_OPTIONS[basis_index]
api_post(session_url("/inverse3/0/config/basis"), {"permutation": perm})
static void post_basis() {
http_post_json(session_url("/inverse3/0/config/basis"),
{{"permutation", BASIS_OPTIONS[basis_index].first}});
}
struct basis_body { std::string permutation; };
static void post_basis() {
http_post_json(session_url("/inverse3/0/config/basis"),
basis_body{BASIS_OPTIONS[basis_index].first});
}
프리셋
POST /*inverse/0/config/preset?session=:my_profile:0
Content-Type: application/json
{"preset": "arm_front_centered"}
응답: {"ok": true, "data": {"preset": "arm_front_centered"}}
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_preset():
preset = PRESET_OPTIONS[preset_index]
api_post(session_url("/inverse3/0/config/preset"), {"preset": preset})
static void post_preset() {
http_post_json(session_url("/inverse3/0/config/preset"),
{{"preset", PRESET_OPTIONS[preset_index]}});
}
struct preset_body { std::string preset; };
static void post_preset() {
http_post_json(session_url("/inverse3/0/config/preset"),
preset_body{PRESET_OPTIONS[preset_index]});
}
마운트
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": { ... }}} — 정규화 이후의 유효 transform을 그대로 돌려줍니다.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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)
static void post_mount() {
http_post_json(session_url("/inverse3/0/config/mount"), {
{"transform", {
{"position", {{"x", mount_pos[0]}, {"y", mount_pos[1]}, {"z", mount_pos[2]}}},
{"rotation", quat_from_euler_deg(mount_rot[0], mount_rot[1], mount_rot[2])},
{"scale", {{"x", mount_scale[0]}, {"y", mount_scale[1]}, {"z", mount_scale[2]}}},
}},
});
}
struct vec3 { float x{}, y{}, z{}; };
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct transform_t { vec3 position{}; quat rotation{}; vec3 scale{1.0f, 1.0f, 1.0f}; };
struct mount_body { transform_t transform; };
static void post_mount() {
http_post_json(session_url("/inverse3/0/config/mount"), mount_body{
transform_t{
.position = vec3{mount_pos[0], mount_pos[1], mount_pos[2]},
.rotation = quat_from_euler_deg(mount_rot[0], mount_rot[1], mount_rot[2]),
.scale = vec3{mount_scale[0], mount_scale[1], mount_scale[2]},
}});
}
mount와 preset은 상호 배타적입니다한쪽을 POST하면 장치에서 다른 쪽 설정이 지워집니다. 튜토리얼은 이를 명시적으로 추적하지 않습니다 — 각 POST는 독립적이며, 충돌은 서버가 해결합니다. WebSocket 관점에서 본 같은 규칙은 튜토리얼 07을 참조하십시오.
DELETE 초기화 — 세 번의 호출
reset은 설정 키마다 DELETE를 한 번씩 호출합니다. 각 호출은 data에 이제 기본값이 된 값을 담아 200을 반환합니다.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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"))
static void reset_all() {
http_delete(session_url("/inverse3/0/config/basis"));
http_delete(session_url("/inverse3/0/config/preset"));
http_delete(session_url("/inverse3/0/config/mount"));
}
static void reset_all() {
http_delete(session_url("/inverse3/0/config/basis"));
http_delete(session_url("/inverse3/0/config/preset"));
http_delete(session_url("/inverse3/0/config/mount"));
}
마운트 회전 구성
transform.rotation은 전송 형식에서 단위 쿼터니언입니다. 튜토리얼은 회전을 Z-Y-X 내재(intrinsic) 오일러 삼중항으로 저장합니다 (X축 기준 pitch, Z축 기준 yaw, Y축 기준 roll — 모두 도 단위). 그리고 모든 POST마다 쿼터니언을 다시 구성합니다.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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,
}
static json quat_from_euler_deg(float pitch_x, float yaw_z, float roll_y) {
constexpr float deg_to_rad = 3.14159265358979323846f / 180.0f;
const float hx = pitch_x * 0.5f * deg_to_rad;
const float hy = roll_y * 0.5f * deg_to_rad;
const float hz = yaw_z * 0.5f * deg_to_rad;
const float cx = std::cos(hx), sx = std::sin(hx);
const float cy = std::cos(hy), sy = std::sin(hy);
const float cz = std::cos(hz), sz = std::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},
};
}
static quat quat_from_euler_deg(float pitch_x, float yaw_z, float roll_y) {
constexpr float deg_to_rad = 3.14159265358979323846f / 180.0f;
const float hx = pitch_x * 0.5f * deg_to_rad;
const float hy = roll_y * 0.5f * deg_to_rad;
const float hz = yaw_z * 0.5f * deg_to_rad;
const float cx = std::cos(hx), sx = std::sin(hx);
const float cy = std::cos(hy), sy = std::sin(hy);
const float cz = std::cos(hz), sz = std::sin(hz);
return quat{
.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 내재(intrinsic) 방식(q = q_z * q_y * q_x)입니다. 즉, 먼저 X축을 중심으로 피치(pitch)를 적용하고, 다음으로 Y축을 중심으로 롤(roll)을, 마지막으로 Z축을 중심으로 요(yaw)를 적용합니다.
튜토리얼은 상태 표시줄마다 유도된 쿼터니언을 오일러 각 값과 함께 출력하므로, 장치가 회전하기 전에 합성 결과를 검증할 수 있습니다. 로컬 오일러 상태는 세션에 이미 저장된 값과 관계없이 (0, 0, 0)에서 시작합니다. 첫 번째 mount POST가 기존 값을 덮어씁니다.
입력 모델 (간략히)
HTTP 연결 구조가 핵심이며, 키보드 UX는 부차적입니다. 다음 두 가지 의도적인 단순화 방식을 사용합니다.
- Python 구현에서는
keyboard패키지를 사용합니다. 크로스 플랫폼이며 키를 누르고 있을 때의 반복 입력을 기본적으로 처리합니다. 방향키,Page Up/Page Down,=/-키를 누르고 있는 동안 마운트 축이 이동하며,B및P키는 상승 에지(rising edge)에서 기저와 프리셋을 순환합니다. - C++ 구현에서는
std::getline(std::cin, ...)과 간결한 토큰 문법(x+20,sx-5,u+10)을 사용합니다. 연속적인 미세 조정에는 덜 편리하지만, 플랫폼별 콘솔 API에 대해#ifdef를 사용하지 않고도 이식이 가능합니다.
출처
튜토리얼 08도 SDK와 함께 로컬에 설치됩니다. 서비스 설치 디렉터리 아래의 tutorials/08-haply-inverse-http-remote-config/ 경로를 확인하십시오.
관련 문서: 세션 — 원격 제어 · 셀렉터 · 장치 구성 · 기저 순열 · 마운트 및 워크스페이스 · JSON 규약 · 튜토리얼 00 — 장치 목록 · 튜토리얼 07 — 기저 및 마운트 (WebSocket 버전)