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. 참조 대화 — 프로필 이름。
키 바인딩
- 파이썬
- C++
| 버튼 | 작업 |
|---|---|
B | 순환 전환, basis 치환 |
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+ 기본값은 5입니다.) |
sx+[N] … sz-[N] | 개별 축에 대해 비균일 확대/축소(백분율)를 적용하는 바로 가기 기능은 항상 사용할 수 있습니다. |
u+[N] / u-[N] | 세 축을 동시에 ±N %로 균일하게 확대/축소 |
reset | 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 보조 함수
이는 앞서 언급한 세 가지 동사를 간결하게 묶은 것으로, 이 튜토리얼의 나머지 부분을 읽을 때 비즈니스 로직에 더 가깝게 느껴지도록 하기 위함입니다:
- 파이썬
- C++ (nlohmann)
- C++ (Glaze)
파이썬 사용 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}"
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 요청을 처리할 수 있습니다. 마찬가지로 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 의 프로필 이름 와일드카드 패턴. 이 튜토리얼에서는 해당 문자열을 그대로 전달하며, 해석은 서버 측에서 담당합니다.
- 파이썬
- 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번째”를 의미합니다.왼손 “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 요청은 다음을 반환합니다. 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 내회전 오일러 각 3원조(X축을 중심으로 한 피치(pitch), Z축을 중심으로 한 요(yaw), Y축을 중심으로 한 롤(roll), 모두 도 단위)로 저장하고, 매 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 내회전 (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를 구분함으로써 이식성을 높였습니다.
소스 코드
튜토리얼 08도 SDK와 함께 로컬에 설치됩니다. 서비스 설치 디렉터리 아래에서 확인할 수 있습니다. tutorials/08-haply-inverse-http-remote-config/ 에서 찾았습니다.
관련 내용: 세션 — 원격 제어 · 선택기 · 장치 구성 · 베이스 교체 · 마운트 및 작업 공간 · JSON 규칙 · 튜토리얼 00 — 장치 목록 · 튜토리얼 07 — 베이스 및 마운트 (WebSocket 버전)