07. 베이스 및 마운트-플레이그라운드
다음 항목을 대화형으로 제어합니다. 마운트 변환 기기의 기능을 설명하고, 다음과 같이 보여줍니다. configure.basis, configure.preset 그리고 configure.mount 프로토콜 수준에서 상호 작용합니다. configure 명령어에 대한 상호작용적 초점을 유지하기 위해 고정된 수평 기반이 유지됩니다.
배울 내용:
- 설정하기 기저 순열 (
"XZY"→ Y축이 위쪽을 향하는 적용 범위) - 선택 사전 설정 (
arm_front) 그리고 그것이 무엇을 구성하는지 파악하기 - 실행 중에 회전 쿼터니언을 사용하여 마운트 덮어쓰기 (키보드 제어)
- 상호 배타성의 법칙:
mount그리고preset같은 블록 내에서 함께 사용할 수 없습니다. 장치 구성 발생 - 일회성
configure-세ман틱: 키를 한 번 누를 때마다 정확히 하나의 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 순서로 합성한 것). 다음을 고려해 보겠습니다.presetweg – 이 두 가지는 프로토콜 수준에서 서로 배타적입니다. - 리셋 버튼 (
R) 덮어쓰기를 취소합니다. 다음 configure 명령은 다시preset뒤로.
매개변수
| 이름 | 표준 | Zweck |
|---|---|---|
BASIS | "XZY" | 축 순열 — Y축이 위를 향하는 적용 프레임 |
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 서로 배타적이다이 서비스는 다음의 차단을 거부합니다. 장치 구성 ab, 이 두 가지를 모두 포함하고 있습니다. 사용자가 마운트를 덮어쓰면 튜토리얼은 preset 이후의 모든 configure 명령어에서 생략됩니다. 다음을 누르면 R wird preset 다음 configure 명령어에서 다시 활성화되고 mount 기각되었다.
C++ 버전은 백그라운드 stdin 스레드에서 줄 단위로 입력을 읽습니다(각 문자를 입력한 후 Enter 키를 누르세요). Python은 다음 패키지를 사용합니다. keyboard 비동기 메인 루프에서 실시간 키 입력을 확인하기 위해 – ENTER 키를 누를 필요는 없습니다. 같은 키, 같은 명령어입니다.
읽은 상태 필드
Aus 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 (ein std::atomic<bool>)을 true로 설정합니다. 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);
튜토리얼 07도 SDK와 함께 로컬에 설치되어 있으며, 다음 위치에서 찾을 수 있습니다. tutorials/07-haply-inverse-basis-and-mount/ 서비스의 설치 디렉터리에서.
관련 항목: 기저 순열 · 마운트 및 작업 공간 · 장치 구성 · 제어 명령 (set_cursor_force) · 유형 (변환) · 튜토리얼 04 (Hello Floor)