08. 원격 세션 구성 도구
다른 곳 (예: 다른 앱, Unity의 씬, 또는 Haply Hub의 데모 등) 에서 이미 실행 중인 세션을, 해당 기기에 대한 HTTP REST 호출을 통해 재구성할 수 있습니다. 이 튜토리얼에서는 웹소켓을 열지 않습니다. 다른 앱이 계속해서 햅틱 피드백을 출력하는 동안, 베이스, 워크스페이스 프리셋 또는 마운트 변환을 변경하기 위해 GET, POST 및 DELETE 요청만 전송됩니다.
사용 사례
- 실행 중인 데모를 실시간으로 조정해 보세요. Haply Hub에서 Orb 데모를 시작한 다음, 별도의 터미널에서 이 튜토리얼을 실행하여 기본 순열을 바꾸거나, 워크스페이스 사전 설정을 변경하거나, 마운트 변환을 약간 이동해 보세요. 데모를 중지할 필요 없이 Orb 장면의 좌표계가 즉시 이동합니다.
- 사용자별 작업 공간 보정. 호스트 컴퓨터에서 햅틱 시나리오를 실행하고, 동일한 네트워크에 있는 운영자가
mount-오프셋 / 회전 / 크기 조정을 적용하여 가상 작업 공간이 사용자의 책상과 일치하도록 합니다. - 기기 선택이 포함된 옵션 메뉴. 동일한 HTTP 헬퍼를 사용하여
GET /devices조회하기 (참조: 튜토리얼 00), 세션의 웹소켓을 건드리지 않고도 장치를 나열하고 대화형 메뉴를 구성할 수 있습니다(장치를 선택한 후 재구성). 이 튜토리얼에서는 다음과 같이 묻습니다./sessionsab 및 하드코딩*inverse/0; 더 큰 것으로의 전환/devices그러나 제어된 선택은 국부적인 변경입니다. - 스크립트 기반 재구성. 녹화가 시작되는 세션 전에 사전 단계(기본 설정, 프리셋 및 마운트 설정)를 자동화할 수 있으며, 각 클라이언트에 설정을 일일이 적용할 필요가 없습니다.
필수 조건
튜토리얼 08에서는 이미 실행 중인 세션을 재구성합니다. 이를 위해서는 활성 상태인 햅틱 세션이 하나 필요하며, 다른 튜토리얼, Unity의 씬, 또는 Haply Hub의 데모 등 어떤 것이든 상관없습니다.
Haply 허브를 열고, Orb 데모를 실행한 다음, 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 호출이 전송됩니다.
프로필 이름이 없는 세션은 숫자 ID를 통해서만 참조할 수 있으며, 이 ID는 실행할 때마다 변경됩니다. 메인 애플리케이션이 첫 번째 메시지를 보낼 때 session.configure.profile.name 호출하십시오. 그런 다음 다음과 같은 안정적인 선택기를 사용할 수 있습니다. --session :my_profile:0 모든 실행에 걸쳐 재사용할 수 있습니다. 참조: 세션 — 프로필 이름.
키 배열
- 파이썬
- C++
| 맛 | 액션 |
|---|---|
B | 기저 순열 연결 |
P | 작업 공간 사전 설정을 전환하기 |
W / E / R | 마운트 가공 모드 선택 — 위치 (mm) / 회전 (°) / 크기 조정 (%) |
← / → | 현재 모드에서 한 단계 −X / +X |
↑ / ↓ | 현재 모드에서 +Y / −Y 한 단계 |
Page Up / Page Down | 현재 모드에서 +Z / −Z 한 단계 |
= / - | 세 축 모두에서 동시에 균일한 스케일링 ± (항상 사용 가능) |
Delete | DELETE 기본값 + 사전 설정 + 마운트 — 기기 기본값으로 재설정 |
H | 도움말 보기 |
Esc | 종료 (Ctrl+C (이것도 작동합니다) |
줄 기반 — 명령어를 입력한 후 Enter 키를 누르세요.
| 명령 | 액션 |
|---|---|
b | 기저 순열 연결 |
p | 작업 공간 사전 설정을 전환하기 |
w / e / r | 마운트 가공 모드 선택 — 위치 (mm) / 회전 (°) / 크기 조정 (%) |
x+[N] … z-[N] | 현재의 축은 ~을 중심으로 N 활성 모드의 자연 단위 내에서 이동 (x+ ohne Wert = 표준 5) |
sx+[N] … sz-[N] | 한 축에서 불균일한 확대/축소(퍼센트)를 위한 단축키, 항상 사용 가능 |
u+[N] / u-[N] | 세 축 모두에서 동시에 ± N %의 균일한 스케일링 |
reset | DELETE 베이스 + 프리셋 + 마운트용 |
h | 도움말 보기 |
종료하려면 Ctrl+C를 누르십시오(또는 Ctrl+D / EOF).
HTTP 메서드 — GET, POST, DELETE
이 튜토리얼에서는 세 가지 HTTP 메서드만을 사용합니다. 각 호출은 표준 JSON-Envelope ({"ok": true, "data": {...}} 성공할 경우, {"ok": false, "error": "..."} (오류 발생 시) 및 다음 세 가지 상태 코드 중 하나: 200 성공, 400 잘못된 요청, 404 Selektor에서 아무것도 찾지 못했습니다.
| 방법 | Zweck | 사용된 경로 |
|---|---|---|
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)
파이썬은 requests.Session() HTTP Keep-Alive를 위한 설정 (요청당 지연 시간을 ~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 stellt requests::get / requests::post / requests::Delete bereit (큰 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을 감싸며, 마찬가지로 HttpRequest-Muster는 다음을 사용하여 POST 요청을 처리합니다. 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기재됨 → einGET /sessions/<SELECTOR>.200→ 사용;404→ 오류를 표시합니다.- 깃발 없음 →
GET /sessions(목록) → 프로필 이름이 포함된 세션 표시 → 인덱스 조회 → 최종 선택자 생성 (권장):profile:0, 가능한 경우; 그렇지 않은 경우#id).
SELECTOR 다음에 해당하는 모든 형식을 허용합니다. Selektoren — 세션 선택기 다음과 같이 정의되어 있습니다: :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;
}
응답 구조를 Structs로 매핑하면, 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을 기점으로 하는 인덱스입니다. — 이 튜토리얼에서는 항상 첫 번째 역함수만을 다룹니다.
다른 기기로 전환하려면 문자열 하나만 변경하면 됩니다. 다음을 전달하세요. --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 wird vor 색인화에 적용됨 — *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>'. aus
로 시작하여 다음 상태로 끝납니다. 1. 이때 구식 기본값을 사용하는 키보드 루프는 시작되지 않습니다.
전체 구문은 다음에서 확인하실 수 있습니다. Selektoren — 장치 선택기. 고정된 값 대신 장치 선택 메뉴를 만들려면, 다음을 사용하여 장치 수를 세십시오. GET /devices?session=<selector> auf (튜토리얼 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"}}
- 파이썬
- 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-초기화 — 세 번의 호출
reset 구성 키당 하나의 DELETE 요청을 전송합니다. 각 요청은 200 이제 기본값으로 설정된 값을 사용하여 data.
- 파이썬
- 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 순서(X축 주위의 피치, Z축 주위의 요, Y축 주위의 롤 — 모두 도 단위)의 내재적 오일러 3원조로 저장하며, 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,
};
}
해밀턴의 표준 쿼터니언, 오른손 규칙, 스칼라 우선 (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용 분기 구조를 이식 가능하게 함.
소스 코드
튜토리얼 08도 SDK와 함께 로컬에 설치됩니다. 서비스의 설치 디렉터리에서 다음 경로를 찾아보세요. tutorials/08-haply-inverse-http-remote-config/.
관련 항목: 세션 — 원격 제어 · 선택기 · 장치 구성 · 기본 순열 · 마운트 및 작업 공간 · JSON 규칙 · 튜토리얼 00 — 장치 목록 · 튜토리얼 07 — 기본 및 마운트 (WebSocket 버전)