06. 통합 (Inverse3 + 무선 VerseGrip)
2. 이 튜토리얼은 기기 사용법에 관한 것입니다. 그립을 특정 방향으로 향하게 한 상태에서 버튼을 계속 누르고 있으면, 커서가 그 방향으로 이동합니다. Inverse3 커서는 구형의 작업 공간 내에서만 움직일 수 있습니다.
학습 내용:
- 동일한 state 프레임 내의 두 가지 유형의 장치(
inverse3그리고wireless_verse_grip)을 읽어들이기 - 쿼터니언 그립의 방향(로컬의
+Y(축)을 꺼낸다 set_cursor_position를 사용하여 계산된 목표 지점으로 커서를 이동합니다- 타깃을 안전한 작업 공간 구역 내로 제한한다. `Minverse `는 ` Inverse3 `보다 작은 반경을 사용한다.
- 작업 공간 사전 설정(
arm_front_centered)를 설정하여 원점이 도달 범위의 중앙에 오도록 한다
워크플로우
- 두 기기를 모두 감지합니다:
- C++ 이 버전은 시작 시 HTTP를 통해
GET /devices을 조회한 후, 보정 프롬프트를 표시하고 ENTER 키 입력을 기다립니다. - 파이썬은 첫 번째 WebSocket 상태 프레임에서 두 장치의 ID를 읽어옵니다.
- C++ 이 버전은 시작 시 HTTP를 통해
- 첫 번째 메시지에서 세션 프로필 을 등록하고,
configure.preset: arm_front_centered을 설정합니다(원샷 핸드셰이크). - 각 틱마다, 그립의
orientation그리고buttons.{a, b}의 상태를 읽어옵니다. - 이동 버튼이 눌려 있는 경우, 그립의 월드 공간에서의 방향(
R(q) · ĵ、회전 후의 (단위 + Y축)을 계산하고,SPEED로 스케일링된 값을 대상 위치에 더합니다. - 타깃을 작업 공간 구 내로 제한하고,
set_cursor_position로 전송합니다. - (Python 전용) 기기의
config.type에 따라 공의 반지름을 조정합니다.minverse의 경우 0.04 m, 그 외의 경우는 0.10 m입니다.
매개변수
| 이름 | 기본값 | 용도 |
|---|---|---|
SPEED | 0.01 m/tick | 버튼을 누르고 있는 동안의 이동 단계 |
RADIUS_INVERSE3 | 0.10 m | Inverse3 / Inverse3x용 작업 공간 제한 반경 |
RADIUS_MINVERSE | 0.04 m | Minverse 용도별 작업 공간 제한 반경 (Python 전용 — C++의 경우 0.10 (고정) |
PRINT_EVERY_MS | 200 | 텔레메트리 전송 빈도 제한 |
| 세션 프로필 이름 | co.haply.inverse.tutorials:combined | Haply Hub에서 이 시뮬레이션을 식별 |
- Inverse3 자동 보정을 수행합니다(또는 그립을 잉크통에 올려놓고 LED가 켜질 때까지 기다립니다).
- 잉크병에서 그립을 분리합니다.
- A 또는 B 버튼을 누른 상태에서 그립을 돌립니다. 커서는 그립이 향하는 방향으로 이동합니다.
state 필드 읽기
각 틱의 state 프레임에서 다음 내용을 읽어옵니다:
data.inverse3[0].state.cursor_position—vec3data.wireless_verse_grip[0].state.orientation—quaterniondata.wireless_verse_grip[0].state.buttons.{a, b, c}— 참/거짓 값- (파이썬, 첫 번째 프레임만)
data.inverse3[0].config.type— Inverse3 와 Minverse 중 어느 반지름을 사용할지 선택 - (파이썬, 첫 번째 프레임만)
data.inverse3[0].status.calibrated— false인 경우 사용자에게 메시지를 표시합니다
보내기 / 받기
쿼터니언을 이용한 방향 구하기 계산(R(q) 에서 +Y (회전)과 구를 이용한 클램핑은 선형 대수학에서 흔히 다루는 계산입니다. 자세한 내용은 소스 파일을 참조하십시오. Inverse API 측에서는 핸드셰이크와 각 틱마다의 set_cursor_position 를 호출합니다.
- 파이썬
- C++ (nlohmann)
- C++ (Glaze)
단일 비동기 루프입니다. 파이썬은 첫 번째 state 프레임에서 두 장치의 ID를 읽습니다. 핸드셰이크에서는 첫 번째 set_cursor_position 프로필과 configure.preset: arm_front_centered 을 부여합니다.
async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)
if first_message:
first_message = False
inverse3_id = data["inverse3"][0]["device_id"]
grip_id = data["wireless_verse_grip"][0]["device_id"]
radius = get_workspace_radius(data["inverse3"][0].get("config", {}))
# Handshake: profile + preset + first position command
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": inverse3_id,
"configure": {"preset": {"preset": "arm_front_centered"}},
"commands": {"set_cursor_position": {"position": position}},
}],
}
else:
# Per tick: update position from grip pointing direction (classic math, not shown), send
request_msg = {
"inverse3": [{
"device_id": inverse3_id,
"commands": {"set_cursor_position": {"position": position}},
}],
}
await websocket.send(json.dumps(request_msg))
C++은 실행 시에 GET /devices 이를 통해 두 장치의 ID를 가져온 후(HTTP를 통해), WebSocket을 연결합니다.onmessage 는 libhv의 I/O 스레드에서 실행되며, 메인 스레드는 ENTER 입력을 기다리며 차단됩니다.
// Startup (synchronous):
const std::string inv3_device_id = get_first_device_id("inverse3");
const std::string grip_device_id = get_first_device_id("wireless_verse_grip");
// Per tick:
ws.onmessage = [&](const std::string &msg) {
json data = json::parse(msg);
// ... classic math (not shown): update local Inverse3State + WirelessVerseGripState,
// compute new position from grip orientation, clamp to sphere ...
json command;
command["inverse3"] = json::array();
command["inverse3"].push_back({
{"device_id", inv3_device_id},
{"commands", {{"set_cursor_position",
{{"position", {{"x", pos.x}, {"y", pos.y}, {"z", pos.z}}}}}}}
});
if (first_message) {
first_message = false;
command["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:combined"}}}}}};
command["inverse3"][0]["configure"] = {
{"preset", {{"preset", "arm_front_centered"}}}};
}
ws.send(command.dump());
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
libhv의 콜백 모델도 마찬가지입니다. 타입이 지정된 구조체로, state 프레임과 전송할 명령을 모두 표현합니다.devices_message 그 중 2개 std::vector<> 에 의해, 단일 glz::read 이를 통해 두 가지 기기 유형을 모두 확인할 수 있습니다.
// Struct models
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct button_state { bool a{}, b{}, c{}; };
struct wvg_state { quat orientation{}; uint8_t hall{}; button_state buttons{}; };
struct wvg_device { std::string device_id; wvg_state state; };
struct inverse_state { vec3 cursor_position{}, cursor_velocity{}; };
struct inverse_device { std::string device_id; inverse_state state; };
struct devices_message {
std::vector<inverse_device> inverse3;
std::vector<wvg_device> wireless_verse_grip;
};
struct set_cursor_position_cmd { vec3 position; };
struct commands_message {
std::optional<session_cmd> session;
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
const auto &wvg = data.wireless_verse_grip[0].state;
cursor_pos = data.inverse3[0].state.cursor_position;
// ... classic math (not shown): if (wvg.buttons.a || wvg.buttons.b)
// move in pointing dir; clamp to sphere ...
commands_message out_cmds{};
device_commands dc{ .device_id = inv3_device_id };
dc.commands.set_cursor_position = set_cursor_position_cmd{cursor_pos};
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = combined */ };
dc.configure = device_configure{ .preset = preset_cfg{"arm_front_centered"} };
}
out_cmds.inverse3.push_back(std::move(dc));
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
튜토리얼 06도 SDK와 함께 로컬에 설치되어 있습니다. 서비스의 설치 디렉터리 아래에 있는 tutorials/06-haply-inverse-combined/ 를 참조하십시오.
관련: 제어 명령어(set_cursor_position) · 형(쿼터니언, vec3) · 마운트 및 작업 공간(사전 설정) · 튜토리얼 03 (무선 VG) · 튜토리얼 05 (위치 제어)