07. 베이시스 & 마운트 놀이터
장치의 기능을 대화형으로 제어합니다 마운트 변환 그리고 어떻게 하는지 보여줍니다 configure.basis, configure.preset및 configure.mount 와이어에서 함께 작업합니다. 수평 바닥을 고정시켜 두어, 구성 명령어에 대한 상호작용의 초점이 유지되도록 합니다.
배울 내용:
- 설정 기초 순열 (
"XZY"→ Y-up 적용 프레임) - 선택하기 사전 설정 (
arm_front) 및 해당 설정이 무엇을 구성하는지 이해하는 것 - 실행 시점에 회전 쿼터니언을 사용하여 마운트 재정의하기 (키보드 제어)
- 상호 배제 법칙:
mount그리고preset같은 곳에 공존할 수 없다 장치 구성 블록 - 단편
configure의미: 키를 누를 때마다 정확히 하나의 구성 메시지가 전송됩니다 - (C++ Glaze) 배타적 필드 모델링
std::optional
작업 흐름
- 첫 번째 메시지에: 다음을 보내세요 세션 프로필,
configure.basis: "XZY"및configure.preset: arm_front. 보내기 시작set_cursor_force고정 바닥용. - 각 틱마다: 커서의 Y 좌표를 읽고, 계산한다
force_y = max(0, (floor_pos - y) * stiffness), 보내주세요. - 사용자가 마운트 회전 키를 누르면, 플래그
pending_configure = true. - 다음 틱에: 다음을 생성합니다
configure.mount변환이 적용된 블록으로rotation는 단위 쿼터니언(현재 피치와 요의 Z-X 순서 합성)입니다. 생략preset— 이 두 가지는 회선상에서 서로 배타적입니다. - 재설정 키 (
R)는 재정의 설정을 초기화하며, 다음 configure 단계에서는preset또.
매개변수
| 이름 | 기본값 | 목적 |
|---|---|---|
BASIS | "XZY" | 축 순열 — Y-up 적용 프레임 |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | 명명된 프리셋 — 기기의 바닥면을 원점으로 함 |
FLOOR_POS_Y | 0.0 m | 고정 바닥면 (Y축 적용) |
STIFFNESS | 1000 해당 없음 | 바닥 스프링 상수 |
MOUNT_STEP_DEG | 10° | 키를 누를 때마다 회전 |
PRINT_EVERY_MS | 200 | 텔레메트리 스로틀 |
조작법
| 키 | 액션 |
|---|---|
W / S | 장치 +X축을 중심으로 마운트를 ±10° 회전(피치) |
A / D | 장치 +Z축을 중심으로 마운트를 ±10° 회전(요) |
R | 마운트 초기화 — 사전 설정으로 복원 |
H | 제어 항목 표시 |
Q | 종료 |
mount 그리고 preset 서로 배타적이다이 서비스는 장치 구성 두 가지를 모두 포함하는 블록. 사용자가 마운트를 재정의하면, 튜토리얼에서는 preset 이후의 모든 configure 단계에서. 누르면 R 다시 활성화 preset 다음 configure 및 drops 시 mount.
C++ 변형은 백그라운드 stdin 스레드에서 줄 단위로 입력을 읽습니다(각 문자를 입력한 후 Enter 키를 누르세요). Python은 keyboard 메인 비동기 루프에서 실시간 키 폴링을 수행하는 패키지 — 엔터 키를 누를 필요가 없습니다. 동일한 키, 동일한 명령어입니다.
상태 필드 읽기
발신자 data.inverse3[i].state:
cursor_position.y—vec3, 바닥 관통 깊이를 계산하는 데 사용됨current_cursor_force— 원격 측정용으로 보고됨
보내기 / 받기
페이로드의 형태는 모든 변형에서 동일하며, 흥미로운 차이점은 각 변형이 상호 배타적인 방식을 어떻게 구성하는지에 있다. mount / preset 분기 처리 및 입력 스레드가 WebSocket 스레드에 신호를 보내는 방식.
- 파이썬
- C++ (nlohmann)
- C++ (Glaze)
다음과 같은 실시간 키 폴링을 사용하는 단일 비동기 루프 keyboard 패키지. pending_configure 키 핸들러에 의해 설정되는 전역 플래그이며, 매번 configure 블록이 전송됩니다.
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["inverse3"][0]["device_id"]
# Handshake: profile + basis + preset
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": device_id,
"configure": build_configure_block(first_handshake=True),
# -> {"basis": {"permutation": "XZY"},
# "preset": {"preset": "arm_front"}}
}],
}
else:
handle_key_inputs() # may set pending_configure = True (classic, not shown)
y = data["inverse3"][0]["state"]["cursor_position"]["y"]
force_y = 0.0 if y > FLOOR_POS_Y else (FLOOR_POS_Y - y) * STIFFNESS
entry = {
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": force_y, "z": 0.0}}},
}
if pending_configure:
entry["configure"] = build_configure_block(first_handshake=False)
# -> {"mount": {...}} OR {"preset": {...}} (never both)
pending_configure = False
request_msg = {"inverse3": [entry]}
await websocket.send(json.dumps(request_msg))
2스레드 모델: 백그라운드 stdin 스레드가 줄을 읽어들이고 뒤집는다 pending_configure (한 std::atomic<bool>); libhv I/O 스레드는 매 틱마다 이를 확인하고 configure 설정된 경우.
std::atomic<bool> pending_configure{false};
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (!data.contains("inverse3") || data["inverse3"].empty()) return;
const bool do_handshake = first_message;
if (first_message) first_message = false;
const bool do_configure = do_handshake || pending_configure.exchange(false);
json request = {};
if (do_handshake) {
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:basis-and-mount"}}}}}};
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
json dev_cmd = {{"device_id", el.value()["device_id"]}};
if (do_configure) {
json cfg = {};
if (do_handshake) cfg["basis"] = {{"permutation", BASIS}};
if (mount_overridden) {
cfg["mount"] = {{"transform", {
{"position", {{"x", 0.0}, {"y", 0.0}, {"z", 0.0}}},
{"rotation", quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)},
{"scale", {{"x", 1.0}, {"y", 1.0}, {"z", 1.0}}},
}}};
} else {
cfg["preset"] = {{"preset", DEVICE_CONFIG_PRESET}};
}
dev_cmd["configure"] = cfg;
}
const float y = el.value()["state"]["cursor_position"]["y"].get<float>();
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dev_cmd["commands"] = {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", force_y}, {"z", 0.0}}}}}};
request["inverse3"].push_back(dev_cmd);
}
ws.send(request.dump());
};
std::thread input_thr(input_thread_func); // stdin reader — flips pending_configure
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
상호 배타성 규칙은 자연스럽게 다음으로 대응된다 std::optional<preset_cfg> 그리고 std::optional<mount_cfg>: 값이 입력된 항목만 직렬화된 JSON에 표시됩니다. 사전 설정 이름은 enum class ~와 함께 glz::meta 서비스가 기대하는 문자열로 변환하는 매핑 규칙입니다.
// The preset set modelled as an enum
enum class device_preset {
defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom,
};
// Glaze meta — serialize the enum as the JSON string the service expects
template <> struct glz::meta<device_preset> {
using enum device_preset;
static constexpr auto value =
enumerate(defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom);
};
// Transform + the configure block
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,1,1}; };
struct preset_cfg { device_preset preset; };
struct basis_cfg { std::string permutation; };
struct mount_cfg { transform_t transform; };
struct device_configure {
std::optional<preset_cfg> preset; // mutually exclusive with mount
std::optional<basis_cfg> basis;
std::optional<mount_cfg> mount; // mutually exclusive with preset
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (data.inverse3.empty()) return;
const bool do_handshake = first_message.exchange(false);
const bool do_configure = do_handshake || pending_configure.exchange(false);
commands_message out_cmds{};
if (do_handshake) {
out_cmds.session = session_cmd{ /* profile = basis-and-mount */ };
}
for (const auto &dev : data.inverse3) {
device_commands dc{ .device_id = dev.device_id };
if (do_configure) {
device_configure cfg{};
if (do_handshake) cfg.basis = basis_cfg{BASIS};
if (mount_overridden) {
cfg.mount = mount_cfg{ .transform = transform_t{
.rotation = quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)}};
} else {
cfg.preset = preset_cfg{DEVICE_CONFIG_PRESET};
}
dc.configure = std::move(cfg);
}
const float y = dev.state.cursor_position.y;
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dc.commands.set_cursor_force = set_cursor_force_cmd{{0.0f, force_y, 0.0f}};
out_cmds.inverse3.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
std::thread input_thr(input_thread_func);
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
관련 기사: 기저 순열 · 마운트 및 작업 공간 · 장치 구성 · 제어 명령어 (set_cursor_force) · 유형 (변환) · 튜토리얼 04 (Hello Floor)