05. 位置制御
set_cursor_positionInverse3 を通じて、カーソルを対象位置の方向へ移動させます。 相互作用モデルは言語によって異なります。 C++では1回限りのランダムな目標を使用するのに対し、Pythonではキーボード操作によって継続的に移動する方式を採用しています。
学習内容:
- 位置モード制御のための
set_cursor_position使用方法 - 同一の基盤コマンドに対する2つの相互作用モデル
- 目標位置をワークスペースの球体内に制限(クランプ)する — MinverseはInverse3よりも小さい半径を使用します
- ワークスペースの中心に原点がくるように、ワークスペースのプリセットを設定する
ワークフロー
C++(ランダムターゲットモデル)
- stdin から行単位で入力されるキー入力(
n,+,-,q)を読み込むバックグラウンド入力スレッドを起動します。 - WebSocketを開きます。最初のステータスフレームで セッションプロファイルを登録して
configure.preset: arm_front_centeredを設定します。球体内部に最初のランダムな目標を生成します(リジェクションサンプリング、半径 0.08 m)。 - 各マティックごとに、現在の目標について
set_cursor_positionコマンドを送信します。カーソルはターゲットを滑らかに追従します。これは、サービスが送信頻度を制限(レート制限)し、補間処理を行っているためです。 - ユーザーが
n入力後にENTERキーを押すと、入力スレッドが新しいランダムな目標を生成します。+/-で速度を調整し、qで終了します。
Python(押下移動モデル)
- WebSocketを開きます。最初のステータスフレームで
status.calibratedを確認し、デバイスがまだキャリブレーションされていない場合は、ユーザーに通知します。 config.typeを読み、ワークスペースの半径を選択します(minverse= 0.04 m、その他 = 0.10 m)。- セッションプロファイルを登録して
configure.preset: arm_front_centeredを設定します。 - 各マティックごとにキーボードの状態(
W/A/S/D/Q/E)をポーリングし、押された各軸に沿ってSPEED目標位置を更新した後、ワークスペースの範囲内にクランプし、set_cursor_positionを送信します。Rを押すと、目標が原点にリセットされます。
パラメータ
| 名前 | デフォルト値 (C++) | デフォルト値 (Python) | 用途 |
|---|---|---|---|
workspace_radius / RADIUS_INVERSE3 | 0.08 m | 0.10 m (Inverse3) / 0.04 m (Minverse) | 目標区の半径 |
speed_step / SPEED | 0.01 / 押す | 0.00005 m / ティック | 相互作用ごとの移動間隔 |
PRINT_EVERY_MS | — | 100 | テレメトリのスロットリング (Python) |
| セッションプロファイル | co.haply.inverse.tutorials:position-control | 同一 | Haply Hubでの識別に使用 |
Pythonの変形は、最初の状態フレームで status.calibratedを確認し、デバイスがキャリブレーションされていない場合はユーザーに通知します。C++版では、キャリブレーションがすでに完了していることを前提としています。
読み込み中のステータスフィールド
data.inverse3[0].device_id— コマンドの生成に使用data.inverse3[0].state.cursor_position— テレメトリ- (Python、最初のフレームのみ)
data.inverse3[0].config.type—Inverse3 またはMinverse から半径を選択してください - (Python、最初のフレームのみ)
data.inverse3[0].status.calibrated— 値が false の場合は、ユーザーに通知する
送受信
通信ワークフロー
- **C++**は、バックグラウンドでstdinスレッドを実行して
std::atomic<float>目標値を記録すると、WebSocketスレッドは各ティックごとにこの値を読み取ります。n入力後にENTERキーを押すと、入力スレッドが新しいランダムな目標を生成し、qをクリックすると、2つのスレッドが両方とも終了します。 - Pythonはシングルスレッドの非同期方式です — WebSocketループが各ティックごとにキーボードの状態をポーリングし、
positionを直接更新します。
Inverse-APIのペイロードは同じです。最初のティックには、セッションプロファイルと configure.presetこれを含み、その後のティックには set_cursor_positionのみが含まれます。
- Python
- C++ (nlohmann)
- C++ (Glaze)
単一の非同期ループです。キーボードポーリング(handle_keys)は各ティックごとにインラインで実行され、別途スレッドは使用しません。 config.typeと status.calibratedは、最初の状態フレームで一度だけ読み取ります。
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"]
radius = get_workspace_radius(data["inverse3"][0].get("config", {}))
# Handshake: profile + preset (one-shot)
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": device_id,
"configure": {"preset": {"preset": "arm_front_centered"}},
}],
}
else:
# Per tick: update position from keyboard (classic polling, not shown), send command
position = handle_keys(position, radius)
request_msg = {
"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_position": {"position": position}},
}],
}
await websocket.send(json.dumps(request_msg))
これは2スレッドモデルです。バックグラウンドスレッドがstdinを読み込み、 std::atomic<float> 目標値を記録し、libhv I/O スレッドは各ティックごとに ws.onmessageを実行して、atomicの値を読み取ります。
// Shared state written by the stdin thread, read by the ws thread
static std::atomic<float> target_x{0.0f}, target_y{0.0f}, target_z{0.0f};
ws.onmessage = [&](const std::string &message) {
const json data = json::parse(message);
if (data["inverse3"].empty()) return;
json request = {};
const bool do_handshake = first_message.exchange(false);
if (do_handshake) {
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:position-control"}}}}}};
generate_random_target();
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
json dev = {
{"device_id", el.value()["device_id"]},
{"commands", {{"set_cursor_position",
{{"position", {{"x", target_x.load()},
{"y", target_y.load()},
{"z", target_z.load()}}}}}}},
};
if (do_handshake)
dev["configure"] = {{"preset", {{"preset", "arm_front_centered"}}}};
request["inverse3"].push_back(dev);
}
ws.send(request.dump());
};
ws.open("ws://localhost:10001");
std::thread input_thr(input_thread_func); // stdin reader — writes target atomics
while (running) std::this_thread::sleep_for(100ms);
これは2つのスレッドモデルが同一であるということです。命令には、型が指定された構造体が使用されます — std::optional<device_configure>各デバイスごとの1回限りのプリセットが保存され、それ以降のティックではJSONから省略されます。
// Struct models
struct vec3 { float x{}, y{}, z{}; };
struct set_cursor_position_cmd { vec3 position; };
struct preset_cfg { std::string preset; };
struct device_configure { std::optional<preset_cfg> preset; };
struct device_commands {
std::string device_id;
std::optional<device_configure> configure; // one-shot
struct commands_t {
std::optional<set_cursor_position_cmd> set_cursor_position;
} commands;
};
struct commands_message {
std::optional<session_cmd> session; // one-shot
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
commands_message request;
const bool do_handshake = first_message.exchange(false);
if (do_handshake) {
request.session = session_cmd{ /* profile = position-control */ };
}
for (const auto &dev : data.inverse3) {
device_commands dc{ .device_id = dev.device_id };
dc.commands.set_cursor_position = set_cursor_position_cmd{
.position = {target_x.load(), target_y.load(), target_z.load()}};
if (do_handshake)
dc.configure = device_configure{ .preset = preset_cfg{"arm_front_centered"} };
request.inverse3.push_back(std::move(dc));
}
std::string out;
(void)glz::write_json(request, out);
ws.send(out);
};
ws.open("ws://localhost:10001");
std::thread input_thr(input_thread_func);
while (running) std::this_thread::sleep_for(100ms);
チュートリアル 05 も SDK とともにローカルにインストールされています。サービスのインストールディレクトリ内の tutorials/05-haply-inverse-position-control/で確認できます。
関連文書: 制御コマンド (set_cursor_position) ・ マウント&ワークスペース(プリセット) ・ 型 (vec3) ・ チュートリアル 06(統合版)