03. 무선 VerseGrip 출력
튜토리얼 02와 같은 패턴이지만 무선 VerseGrip용입니다 — 스트리밍되는 상태에 버튼(A/B/C)과 배터리 잔량을 추가합니다.
학습 내용:
- 무선 전용 상태 필드 읽기:
buttons.{a,b,c},battery_level,hall - 독립 관찰자용 keepalive로
probe_orientation사용하기 - 튜토리얼 02와 같은, 첫 메시지에서만 수행하는 핸드셰이크 패턴
작업 흐름
ws://localhost:10001로 WebSocket 연결을 열고 첫 번째 상태 프레임을 기다립니다.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 Hub에서 이 시뮬레이션을 식별 |
변환은 애플리케이션 좌표계 +X right, +Y forward, +Z up에서 내재적 Z-X-Y 순서(yaw → pitch → roll)를 따릅니다. 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만 포함됩니다.
- Python
- 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 프로토콜