07. ベース&マウント 演習場
インタラクティブな方法でデバイスを駆動する マウント変換,そして実演し configure.basis、configure.preset と configure.mount 通信メッセージレベルでの連携方法。一貫した基準を維持し、対話の対象を常に「configure」コマンドに集中させる。
学習内容:
- 設定 basis 軸の置換(
"XZY"→ Y軸方向の座標系) - 選択 プリセット(
arm_front),そしてそれがどのような設定を行うかを理解する - 実行時に(キーボード操作による)四元数の回転を通じてマウントを上書きする
- 排他ルール:
mountとpreset同じ場所に同時に存在することはできない デバイスの設定 ブロック内 - 使い捨て
configure意味:キーが押されるたびに、configure メッセージを 1 つだけ送信する - (C++ Glaze)の使用
std::optional排他フィールドのモデリング
ワークフロー
- 最初のメッセージを受信したとき:送信 会話プロファイル、
configure.basis: "XZY"およびconfigure.preset: arm_front。地面を固定するために、継続的に送信を開始するset_cursor_force。 - 各ティックごとに:カーソルの Y 座標を読み取り、計算する
force_y = max(0, (floor_pos - y) * stiffness)そして送信します。 - ユーザーが「mount」の回転キーを押すと、
pending_configure = true位置。 - 次のティックで:1つを構築する
configure.mountブロック、その変換されたrotation単位四元数(現在のピッチ角/ヨー角に基づき、Z軸、X軸の順に合成)。省略preset——両者は通信プロトコルにおいて互いに排他的である。 - リセットボタン(
R)上書き設定を削除します。次回の configure 実行時には、再びpreset。
パラメータ
| 名称 | デフォルト値 | 用途 |
|---|---|---|
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 排他サービスは、その両方を同時に含むものを拒否します。 デバイスの設定 ブロック。ユーザーが mount を上書きすると、このチュートリアルではその後の configure 処理のたびにこの手順が省略されます。 preset。押す R その後、次回の configure 実行時に再び有効になります preset そして削除する mount。
C++ のバリアントは、バックグラウンドの stdin スレッドで行単位で入力を読み取ります(各文字の後に ENTER キーを押す必要があります)。一方、Python ではメインの非同期ループ内で keyboard キー入力をリアルタイムでポーリングするため、ENTERキーを押す必要はありません。キーが同じであれば、コマンドも同じです。
読み込まれたステータスフィールド
出典: data.inverse3[i].state:
cursor_position.y—vec3,地中への貫入深度を算出するために用いるcurrent_cursor_force— テレメトリ報告用
送信 / 受信
各バリアントの負荷構造は同じですが、真の違いは、各バリアントが排他制御をどのように構築しているかという点にあります。 mount / preset ブランチ、および入力スレッドが WebSocket スレッドにどのように通知するか。
- Python
- 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 スレッドが入力を1行ずつ読み取り、ビットを設定する 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 には、値が割り当てられたものだけが現れます。デフォルト名は 1 つの 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/。
関連情報: Basis 軸の置換 ・ マウントとワークスペース ・ 設備構成 ・ 制御コマンド(set_cursor_force) ・ タイプ(transform) ・ チュートリアル 04(Hello Floor)