08. 원격 세션 구성 도구
다른 앱, Unity 씬,Haply Hub 데모 등다른 곳에서 이미 실행 중인 세션을재구성합니다. 이는 기기에 HTTP REST 호출을 전송하여 수행됩니다. 이 튜토리얼에서는 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
이 튜토리얼은 실행 시 세션의 현재 basis / preset / mount를 표시한 후, 키 입력을 기다립니다. 키를 누를 때마다정확히 한 번의 REST 호출이 전송됩니다.
프로필 이름이 없는 세션은 숫자 ID로만 지정할 수 있습니다. 이 ID는 실행할 때마다 바뀝니다. 메인 앱의 첫 번째 메시지에서 session.configure.profile.name ~를 호출하도록 하면,--session :my_profile:0 이와 같은 안정적인 선택기를 모든 실행에서 재사용할 수 있습니다. 자세한 내용은 세션 — 프로필 이름 를 참조하십시오.
키 바인딩
- 파이썬
- C++
| 키 | 작업 |
|---|---|
B | basis의 순열을 차례로 바꾸다 |
P | 워크스페이스 사전 설정을 차례로 전환 |
W / E / R | 마운트 편집 모드 선택 — 위치 (mm) / 회전 (°) / 배율 (%) |
← / → | 현재 모드에서 −X / +X 방향으로 이동 |
↑ / ↓ | 현재 모드에서 +Y / −Y 방향으로 이동 |
Page Up / Page Down | 현재 모드에서 +Z / −Z 방향으로 이동 |
= / - | 3축 모두를 한 번에 균일하게 확대/축소 (±) (항상 사용 가능) |
Delete | DELETE basis・preset・mount를 초기화하여 장치를 기본 설정으로 되돌립니다. |
H | 도움말 보기 |
Esc | 종료(Ctrl+C (그것도 가능) |
줄 단위로 입력하십시오. 입력이 끝나면 ENTER 키를 누르십시오.
| 명령어 | 작업 |
|---|---|
b | basis의 순열을 차례로 바꾸다 |
p | 작업 공간 사전 설정을 차례로 전환 |
w / e / r | 마운트 편집 모드 선택 — 위치 (mm) / 회전 (°) / 배율 (%) |
x+[N] … z-[N] | 현재 축을 활성 모드의 단위로 N 단순히 이동(x+ '만'인 경우 기본값인 5) |
sx+[N] … sz-[N] | 1축에 대한 비균일 스케일 단축키(퍼센트). 항상 사용 가능 |
u+[N] / u-[N] | 3축 모두를 한 번에 균일하게 ± N % 확대/축소 |
reset | DELETE basis・preset・mount를 초기화 |
h | 도움말 보기 |
Ctrl+C(또는 Ctrl+D / EOF)를 누르면 종료됩니다.
HTTP 메서드 — GET, POST, DELETE
이 튜토리얼에서는 HTTP 메서드 중 3가지만 사용합니다. 모든 호출은 표준 JSON 엔벨로프 을 반환합니다(성공 시 {"ok": true, "data": {...}}、실패 시에는 {"ok": false, "error": "..."}). 또한, 상태 코드는 다음 3가지 중 하나입니다: 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 헬퍼
위의 세 가지 메서드를 간략하게 감싸서, 튜토리얼의 나머지 부분을 본문의 로직으로 읽을 수 있도록 한 것입니다:
- 파이썬
- C++ (nlohmann)
- C++ (Glaze)
파이썬에서는 HTTP 키프얼라이브를 위해 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 을 공개하고 있습니다(D 대문자입니다.delete (이는 C++의 키워드이기 때문입니다). 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 요청은 하나의 템플릿으로 감싸집니다. POST도 마찬가지입니다. HttpRequest 해당 패턴을 사용하여,glz::write_json 다음과 같이 처리합니다:
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 ~와 같은 프로필 이름의 와일드카드 패턴입니다. 이 튜토리얼에서는 문자열을 그대로 전달하며, 파싱은 서비스 측에서 수행됩니다.
- 파이썬
- 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>
*inverseInverse 제품군에 속하는 임의의 기기(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번째왼손 계열“의 반대”를 의미하며, “0번째의 반대, 아마도 왼쪽에 있는 것”을 뜻하는 것은 아닙니다. 자세한 내용은 선택자 — 인덱스 생성 전에 클래스 필터가 해결됨 를 참조하십시오.
튜토리얼은 실행 시 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 구성 — 기본값, 사전 설정, 마운트
3개의 키는 요청 형식은 동일하지만 본문의 스키마가 다릅니다. 모든 POST 요청은 결과 값을 data ~에 포함되는 200 을 반환합니다. 세션/기기 선택기에 일치하는 항목이 없는 경우 404 을 반환합니다.
기초
POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json
{"permutation": "XZY"}
답변: {"ok": true, "data": {"permutation": "XZY"}}
- 파이썬
- 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"}}
- 파이썬
- 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": { ... }}} — 정규화된 유효한 변환을 그대로 반환합니다.
- 파이썬
- 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를 통한 초기화 — 3회 호출
reset 설정 키마다 한 번씩 DELETE를 실행합니다. 각 호출은 기본값으로 설정된 값을 data ~에 포함되는 200 을 반환합니다.
- 파이썬
- 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 고유 오일러 각도의 3원조(X축 주위의 피치, Z축 주위의 요, Y축 주위의 롤 — 모두 도 단위)로 저장하고, POST가 발생할 때마다 쿼터니언을 재구성합니다.
- 파이썬
- 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,
};
}
단위 해밀턴 4원수, 오른손 좌표계, 스칼라 우선 (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)를 사용하고 있습니다. 지속적인 미세 조정에 사용하기에는 다소 불편하지만, 플랫폼별 콘솔 API를#ifdef분기를 만들 필요가 없기 때문에 이식성이 있습니다.
출처
튜토리얼 08도 SDK와 함께 로컬에 설치되어 있습니다. 서비스의 설치 디렉터리 아래에 있는 tutorials/08-haply-inverse-http-remote-config/ 확인해 주십시오.
관련 항목: 세션 — 원격 제어 · 선택기 · 장치 구성 · 기본값 대체 · 마운트 및 작업 공간 · JSON 규칙 · 튜토리얼 00 — 장치 목록 · 튜토리얼 07 — 기본값 및 마운트 (WebSocket 버전)