02. VerseGrip 출력
첫 번째 유선 VerseGrip에서 방향(쿼터니언 + Z-X-Y 오일러 각도), 홀 센서 레벨, 버튼 상태를 스트리밍합니다.
학습 내용:
- 상태 프레임에서
quaternion방향 파악하기 - 쿼터니언을 도 단위의 Z-X-Y 오일러 각으로 변환하기 (+X는 오른쪽, +Y는 앞쪽, +Z는 위쪽)
probe_orientation을 단독 관찰자용 keepalive로 사용하기- 첫 번째 메시지에만 전송되는 핸드셰이크 패턴 (튜토리얼 01과동일)
워크플로우
ws://localhost:10001WebSocket를 열고 첫 번째 상태 프레임을 기다립니다.verse_grip배열에서 첫 번째 유선 VerseGrip의device_id을 선택합니다.- 세션 프로필및 기기별
probe_orientationkeepalive를 포함한 요청을 생성합니다(상태 프레임에서 그립 방향 데이터가 지속적으로 전송되도록 하는 빈 객체 명령). - 요청을 전송한 뒤
session필드를 제거합니다. 이 필드는 일회성 핸드셰이크용입니다. - 이후 모든 프레임에서 쿼터니언을 오일러 각으로 변환하고, 스로틀된 텔레메트리 데이터를 출력합니다. 매 틱마다 keepalive를 다시 전송합니다.
매개변수
| 이름 | 기본값 | 용도 |
|---|---|---|
URI | ws://localhost:10001 | 시뮬레이션 채널 WebSocket URL |
PRINT_EVERY_MS | 100 | 콘솔 출력 제한 |
| 세션 프로필 이름 | co.haply.inverse.tutorials:print-verse-grip | Haply 허브에서 이 시뮬레이션을 식별 |
변환은 애플리케이션 프레임 +X right, +Y forward, +Z up내재적(intrinsic) Z-X-Y(요 → 피치 → 롤) 방식입니다. glm::eulerAngles는 사용하지 마십시오. 이 함수는 다른 규약을 따르기 때문에 여기에서는 잘못 해석됩니다. 세 가지 언어 버전 모두 동일한 수식을 구현하므로, 수식에 대해서는 소스 코드를 참조하십시오.
probe_orientation실제로 필요한 경우probe_orientation이 세션이 Inverse3에 명령을 보내지 않는 다음과 같은 경우에만 유용합니다. Inverse3(힘, 위치, 토크 등)에 명령을 내리는 순간, 서비스가 페어링된 VerseGrip의 방향 정보를 모든 상태 프레임에 자동으로 스트리밍하므로 프로브가 필요하지 않습니다. probe_orientation이 도구는 본 튜토리얼과 같은 단독 그립 모니터링 도구에만 사용하십시오.
읽기 상태 필드
data.verse_grip[0].state다음에서 발췌:
orientation—quaternion(w, x, y, z)hall— 정수형 홀 센서 값button— 불리언transform.rotation— 작업 공간 회전(quaternion). 방향 전용 장치에서는 위치와 크기가 적용되지 않으므로 절대 출력되지 않습니다.
transform.rotation은 항등 쿼터니언 {w:1,x:0,y:0,z:0}과 같을 때 생략됩니다. 읽을 때는 기본값을 지정하십시오. 항상 값을 받으려면 serialization/explicit_fields을 활성화하십시오.
송신 / 수신
WebSocket 루프: 상태 프레임을 수신하고, 핸드셰이크와 probe_orientation keepalive를 구성하여 다시 전송합니다. 첫 번째 전송 메시지에는 세션 프로필이 포함되며, 그 이후의 모든 프레임에는 keepalive만 포함됩니다.
- 파이썬
- C++ (nlohmann)
- C++ (Glaze)
단일 비동기 루프 — recv() → 명령 구성 → send() → 반복.
async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)
if first_message:
first_message = False
device_id = data["verse_grip"][0]["device_id"]
request_msg = {
"session": {"configure": {"profile": {
"name": "co.haply.inverse.tutorials:print-verse-grip"}}},
"verse_grip": [{
"device_id": device_id,
"commands": {"probe_orientation": {}} # empty — keepalive
}]
}
await websocket.send(json.dumps(request_msg))
request_msg.pop("session", None) # one-shot handshake
libhv는 I/O 스레드에서 WebSocket를 구동합니다 — 프레임별 작업은 ws.onmessage에서 처리됩니다. 메인 스레드는 ENTER 키를 기다리며 차단됩니다.
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (first_message) {
first_message = false;
device_id = data["verse_grip"][0].at("device_id").get<std::string>();
request_msg = {
{"session", {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:print-versegrip"}}}}}}},
{"verse_grip", json::array({
{{"device_id", device_id},
{"commands", {{"probe_orientation", json::object()}}}},
})},
};
}
ws.send(request_msg.dump());
request_msg.erase("session"); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
동일한 libhv 콜백 모델을 사용합니다. 유형이 지정된 구조체가 nlohmann::json을 대체합니다 — probe_orientation_cmd는 빈 구조체입니다(Glaze는 이를 {}로 기록합니다). std::optional<session_cmd>한 번만 수행되는 핸드셰이크를 처리하며, 첫 번째 전송 후 .reset()을 호출하면 이후 JSON 출력에서 이 항목이 생략됩니다.
// Struct models
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct grip_state { quat orientation{}; bool button{}; uint8_t hall{}; };
struct grip_device { std::string device_id; grip_state state; };
struct devices_message { std::vector<grip_device> verse_grip; };
struct probe_orientation_cmd {}; // empty object on the wire
struct commands_message {
std::optional<session_cmd> session; // one-shot — omitted when unset
std::vector<device_commands> verse_grip;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = print-versegrip */ };
device_commands dc{ .device_id = data.verse_grip[0].device_id };
dc.commands.probe_orientation = probe_orientation_cmd{};
out_cmds.verse_grip.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
out_cmds.session.reset(); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
명령줄 플래그 (Python)
Python 변형은 튜토리얼 01과동일한 두 가지 플래그를 지원합니다:
--full— 한 줄 요약 대신 원시 JSON 페이로드를 보기 좋게 출력합니다.--query-config— 매 틱마다session.force_render_full_state를 다시 주입함으로써, 각 프레임이 상태 델타만 전달하는 대신config블록(장치 유형, 펌웨어, 마운트 변환 등)을 포함하도록 합니다. 설정 변경 사항을 실시간으로 디버깅할 때 유용합니다.
이 두 플래그는 함께 사용할 수 있습니다. C++ 변형에서는 이 플래그를 제공하지 않습니다.
튜토리얼 02도 SDK와 함께 로컬에 설치되어 있습니다 — 서비스 설치 디렉터리 아래의 tutorials/02-haply-inverse-print-verse-grip/에서 확인하실 수 있습니다.
관련: 타입 (쿼터니언) · 제어 명령 (probe_orientation) · WebSocket 프로토콜 · 튜토리얼 03 (무선 VG)