03. 무선 VerseGrip 출력
튜토리얼 02와같은 패턴이지만 무선 VerseGrip용입니다 — 스트리밍 중일 때 버튼(A/B/C)과 배터리 잔량을 표시합니다.
학습 내용:
- 무선 전용 상태 필드 읽기:
buttons.{a,b,c},battery_level,hall - 독립 관찰자용 keepalive로
probe_orientation사용 방법 - 튜토리얼 02와같이, 첫 번째 메시지에서만 수행되는 핸드셰이크 패턴
작업 흐름
ws://localhost:10001WebSocket 연결을 열고 첫 번째 상태 프레임을 기다립니다.wireless_verse_grip배열에서 첫 번째 무선 VerseGrip의device_id를 가져옵니다.- 세션 프로필및 기기별
probe_orientationkeepalive를 포함한 요청을 구성합니다. - 요청을 보낸 후
session필드를 제거합니다 — 이는 일회성 핸드셰이크이기 때문입니다. - 이후 모든 프레임에서 쿼터니언을오일러 각으로 변환하고, 버튼 상태와 배터리 잔량을 포함한 텔레메트리 데이터를 주기적으로 제한하여 출력합니다. 매 틱마다 keepalive 신호를 다시 전송합니다.
매개변수
| 이름 | 기본값 | 용도 |
|---|---|---|
URI | ws://localhost:10001 | 시뮬레이션 채널 WebSocket URL |
PRINT_EVERY_MS | 100 | 콘솔 출력 간격 제한 |
| 세션 프로필 이름 | co.haply.inverse.tutorials:print-wireless-verse-grip | Haply 허브에서 이 시뮬레이션을 식별 |
변환은 애플리케이션 좌표계에서 이루어집니다. +X right, +Y forward, +Z up에서 내재적 Z-X-Y 순서(요 → 피치 → 롤)를 따릅니다. glm::eulerAngles를 사용하지 마십시오 — 이 함수는 다른 규약을 따르기 때문에 여기서는 잘못된 값을 읽게 됩니다. 세 가지 언어 변형 모두 동일한 수식을 구현하며, 공식은 소스 코드를 참조하십시오.
probe_orientation실제로 필요한 경우probe_orientation이 세션은 Inverse3에 어떠한 명령도 전송하지 않을 때만 유용합니다. Inverse3에 명령(힘, 위치, 토크 등)을 전송하는 즉시, 서비스가 모든 상태 프레임에 대응하는 VerseGrip의 방향 값을 자동으로 스트리밍하므로 프로브가 필요하지 않습니다. probe_orientation이 튜토리얼에서와 같이 독립형 그립 모니터링 도구에서만 사용하십시오.
읽기 상태 필드
data.wireless_verse_grip[0].state에서 읽은 값:
orientation—quaternion(w, x, y, z)hall— 정수형 홀 센서 값buttons.a,buttons.b,buttons.c— 불리언battery_level— 실수형 (0.0 – 1.0)transform.rotation— 작업 공간 회전 (quaternion). 방향 전용 장치에는 위치와 스케일이 적용되지 않으므로 전혀 전송되지 않습니다.
transform.rotation 값이 항등 쿼터니언인 {w:1,x:0,y:0,z:0}인 경우 생략됩니다. 읽을 때 기본값을 지정하십시오. 항상 수신하려면 serialization/explicit_fields을 활성화하십시오.
송신 / 수신
튜토리얼 02와 같은 구조이며, 장치 배열만 wireless_verse_grip로 변경됩니다. WebSocket 루프는 상태 프레임을 수신하여 핸드셰이크와 probe_orientation keepalive를 다시 전송합니다. 첫 번째 전송 메시지에는 세션 프로필이 포함되며, 그 이후의 모든 프레임에는 keepalive만 포함됩니다.
- 파이썬
- C++ (nlohmann)
- C++ (Glaze)
단일 비동기 루프입니다.
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["wireless_verse_grip"][0]["device_id"]
request_msg = {
"session": {"configure": {"profile": {
"name": "co.haply.inverse.tutorials:print-wireless-verse-grip"}}},
"wireless_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
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (first_message) {
first_message = false;
device_id = data["wireless_verse_grip"][0].at("device_id").get<std::string>();
request_msg = {
{"session", {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:print-wireless-verse-grip"}}}}}}},
{"wireless_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
타입이 지정된 구조체를 사용합니다. 참고로 wvg_state의 buttons 필드가 가려지지 않도록 button_state라는 이름을 명시적으로 사용했습니다(buttons 아니면). Glaze는 필드를 이름으로 매핑하므로 구조체 타입의 이름은 자유롭게 정할 수 있습니다.
// 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{};
float battery_level{};
};
struct wvg_device { std::string device_id; wvg_state state; };
struct devices_message { std::vector<wvg_device> wireless_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> wireless_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;
request_msg.session = session_cmd{ /* profile = print-wireless-verse-grip */ };
device_commands dc{ .device_id = data.wireless_verse_grip[0].device_id };
dc.commands.probe_orientation = probe_orientation_cmd{};
request_msg.wireless_verse_grip.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(request_msg, out_json);
ws.send(out_json);
request_msg.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블록이 포함되도록 합니다.
--query-config는 config 블록은 일반 구성 필드 외에도 WVG 전용 sub_type 필드(stylus / prototype / ruko / kingfisher(3.5 버전에서 추가됨)을 포함하고 있어 무선 VerseGrip에서 특히 유용합니다. python 03-haply-inverse-print-wireless-verse-grip.py --full --query-config을 실행하면 다른 구성 값과 함께 config.sub_type 값이 실시간으로 갱신되는 것을 확인할 수 있습니다.
C++ 변형은 이 플래그를 노출하지 않습니다.
튜토리얼 03도 SDK와 함께 로컬에 설치됩니다. 서비스 설치 디렉터리 아래의 tutorials/03-haply-inverse-print-wireless-verse-grip/ 폴더를 확인해 주십시오.
관련 문서: 튜토리얼 02 (유선 VG) · 유형 (쿼터니언) · 제어 명령 (probe_orientation) · WebSocket 프로토콜