08. リモートセッションコンフィギュレータ
別の場所(たとえば、別のアプリ、Unityシーン、Haply Hubのデモなど)で実行中のセッションを再配置するには、そのデバイスに対してHTTP REST呼び出しを実行します。このチュートリアルではWebSocketを開くことはありません。GET、POST、DELETEリクエストを使用するだけで、別のアプリで触覚フィードバックのレンダリングを継続しながら、basis、ワークスペースプリセット、またはマウント変換を変更することができます。
利用シーン
- 実行中のデモをリアルタイムで微調整します。「Haply 」HubOrbデモを起動し、別のターミナルでこのチュートリアルを実行すると、basis 軸の置換を切り替えたり、ワークスペースのプリセットを変更したり、mount 変換を微調整したりできます。Orb の座標系は即座に変化しますが、デモを停止する必要はありません。
- ユーザーごとにワークスペースのキャリブレーションを行います。 タッチスクリーンシーンをホスト上で実行し続け、同一ネットワーク内のオペレーターがプッシュできるようにする
mountのオフセット/回転/拡大縮小を行い、仮想作業スペースをユーザーのデスクに合わせます。 - デバイス選択機能付きのオプションメニュー。 同じ HTTP ヘルパー関数を呼び出すことができます
GET /devices(参照 チュートリアル 00)デバイス一覧を表示し、インタラクティブなメニューを構築します。デバイスを1つ選択すると、そのデバイスの設定を変更できます。セッション内のWebSocketにアクセスする必要はありません。このチュートリアルでは、/sessionsさらに、ハードコーディングされており、*inverse/0,しかし、代わりに/devicesドライバのセレクタは、単なるローカルな変更に過ぎません。 - スクリプト化による再構成。セッションの録画開始前に、事前チェック手順(basisの設定+プリセット+マウント)を自動的に実行し、各クライアントにこれらの設定を個別に組み込む必要をなくします。
前提条件
チュートリアル 08:実行中のセッションを再構成するためのものです。アクティブなHapticセッション(別のチュートリアル、Unityシーン、またはHaply Hubのデモなど)が1つ必要です。
Haply Hubを開き、 Orbのデモを起動して、それをそのままターゲットとして指定します:
./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"
「Orb」シーンは、デバイスのワークスペース内に球体をレンダリングします。チュートリアル 08 に従って、「basis」やプリセットを切り替えたり、「mount」の変形を微調整したりすると、「Orb」の座標系がリアルタイムで視覚的にずれていきます。
用法
# Pick a session interactively (lists every session the service knows)
./08-haply-inverse-http-remote-config
python 08-haply-inverse-http-remote-config.py
# Target the Haply Hub Orb demo directly
./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"
# Target one directly by selector
./08-haply-inverse-http-remote-config --session :my_profile:0
python 08-haply-inverse-http-remote-config.py --session "#42"
# Or by a wildcard profile pattern (first match) — handy when the exact profile is unknown
./08-haply-inverse-http-remote-config --session "co.haply.hub::*:0"
# Pick a specific device on the session (default is *inverse/0)
./08-haply-inverse-http-remote-config --device 'inverse3/#A12' # by exact id
./08-haply-inverse-http-remote-config --device '*inverse:left/0' # first left-handed inverse
./08-haply-inverse-http-remote-config --device '*inverse:right/0' # first right-handed inverse
./08-haply-inverse-http-remote-config --device '*verse_grip/0' # first grip-family device
チュートリアルは起動時に、セッションの現在の basis / preset / mount を出力し、その後キー入力を待機します。キーが押されるたびに、REST 呼び出しが1 回送信されます。
profile 名が設定されていないセッションは、数値の ID でのみ指定できます。ただし、この ID は実行のたびに変化します。メインアプリでは、最初のメッセージ内で以下を呼び出してください。 session.configure.profile.name,その後、実行のたびに安定したセレクタを再利用できるようになります。例えば、 --session :my_profile:0。参照 会話 — プロフィール名。
キーの割り当て
- Python
- C++
| ボタン | 操作 |
|---|---|
B | 循環切り替え、basis置換 |
P | ワークスペースプリセットの循環切り替え |
W / E / R | マウント編集モードの選択 — 位置(mm)/ 回転(°)/ 拡大率(%) |
← / → | 現在のモードで −X / +X 方向にステップ移動する |
↑ / ↓ | 現在のモードで +Y / −Y 方向にステップ移動する |
Page Up / Page Down | 現在のモードで +Z / −Z 方向にステップ移動する |
= / - | 3つの軸を同時に統一して拡大・縮小する ±(常に利用可能) |
Delete | DELETE ベース + プリセット + マウント — デバイスのデフォルト値に戻す |
H | ヘルプを表示 |
Esc | 終了(Ctrl+C (それも可) |
行ごとに入力 — 入力後、ENTER キーを押してください。
| 命令 | 操作 |
|---|---|
b | ループ切り替え・ベース置換 |
p | ワークスペースのプリセットを順番に切り替える |
w / e / r | マウント編集モードの選択 — 位置(mm)/ 回転(°)/ 拡大率(%) |
x+[N] … z-[N] | 現在のモードの自然単位で、現在の軸に沿ってステップを進める N(個別の x+ デフォルトは 5 です) |
sx+[N] … sz-[N] | 個々の軸を不均一に拡大・縮小するショートカット(パーセンテージ)は、常に利用可能です |
u+[N] / u-[N] | 3つの軸を同時に±N%で統一して拡大・縮小する |
reset | DELETE ベース + プリセット + マウント |
h | ヘルプを表示 |
Ctrl+C(または Ctrl+D / EOF)を押して終了します。
HTTPメソッド — GET、POST、DELETE
このチュートリアルでは、HTTP の 3 種類の動詞を使用しており、これら 3 種類のみを使用しています。呼び出しを行うたびに、標準的な JSON レスポンスのフォーマット(成功した場合は {"ok": true, "data": {...}},失敗した場合は {"ok": false, "error": "..."}),そして以下の3つのステータスコードのいずれかを返します:200 成功、400 リクエスト形式が不正です、404 セレクタに一致する項目が見つかりませんでした。
| 動詞 | 用途 | 使用されたパス |
|---|---|---|
GET | 現在の状態の読み取り — セッション一覧、指定セッションの照会、現在の設定値 | /sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=... |
POST | 設定値の置換 — リクエスト本文が JSON の場合 | /<device_selector>/config/{basis,preset,mount}?session=... |
DELETE | 設定値をデバイスのデフォルト値に戻す | /<device_selector>/config/{basis,preset,mount}?session=... |
HTTP ヘルパー関数
これは、前述の3種類の動詞を簡潔にまとめたもので、このチュートリアルの残りの部分を、よりビジネスロジックらしい内容として読みやすくするためのものです:
- Python
- C++ (nlohmann)
- C++ (Glaze)
Python の使用 requests.Session() HTTP 長接続の再利用を実現(1回のリクエストの遅延を約 50 ms から約 5 ms に短縮可能):
http = requests.Session()
def api_get(path):
r = http.get(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None
def api_post(path, body):
r = http.post(f"{BASE_URL}{path}", json=body, timeout=3)
return r.json() if r.status_code == 200 else None
def api_delete(path):
r = http.delete(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None
def session_url(endpoint):
return f"{endpoint}?session={session_selector}"
libhv 提供 requests::get / requests::post / requests::Delete(注意:頭文字を大文字にする) D — delete (C++ のキーワードです)。POST では、以下を設定するためにリクエストを手動で構築する必要があります。 Content-Type: application/json:
static std::string session_url(const std::string &endpoint) {
return BASE_URL + endpoint + "?session=" + session_selector;
}
static json http_get(const std::string &url) {
auto resp = requests::get(url.c_str());
if (!resp || resp->status_code != 200) return {};
try { return json::parse(resp->body); } catch (...) { return {}; }
}
static bool http_post_json(const std::string &url, const json &body) {
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = url;
req->content_type = APPLICATION_JSON;
req->body = body.dump();
auto resp = requests::request(req);
return resp && resp->status_code == 200;
}
static bool http_delete(const std::string &url) {
auto resp = requests::Delete(url.c_str());
return resp && resp->status_code == 200;
}
レスポンスには常に {"ok", "data": T} パッケージ構造。1つのテンプレートで、すべてのタイプを持つGETリクエストをカプセル化できます。同様に、 HttpRequest パターンの組み合わせ glz::write_json POST メソッドで使用可能:
template <typename T> struct envelope { bool ok{}; T data{}; };
template <typename Payload>
static std::optional<Payload> http_get_envelope(const std::string &url) {
auto resp = requests::get(url.c_str());
if (!resp || resp->status_code != 200) return std::nullopt;
envelope<Payload> env{};
if (glz::read<glz_settings>(env, resp->body)) return std::nullopt;
return std::move(env.data);
}
template <typename Body>
static bool http_post_json(const std::string &url, const Body &body) {
std::string buf;
if (glz::write_json(body, buf)) return false;
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = url;
req->content_type = APPLICATION_JSON;
req->body = std::move(buf);
auto resp = requests::request(req);
return resp && resp->status_code == 200;
}
static bool http_delete(const std::string &url) {
auto resp = requests::Delete(url.c_str());
return resp && resp->status_code == 200;
}
セッション検出 — GET /sessions
によると --session パラメータによって実行パスが決まります:
- 提供されました
--session SELECTOR→ 1回送信するGET /sessions/<SELECTOR>。200→ そのまま使用;404→ エラーが発生し、終了しました。 - 指定されていない引数 →
GET /sessions(リスト)→ プロファイル名付きのセッションを表示 → インデックスの入力を促す → 最終的なセレクタを構築(利用可能な場合は優先して使用):profile:0,そうでない場合は、以下にフォールバックします。#id)。
SELECTOR 支持 セレクタ — セッションセレクタ ここで定義されたすべての形式::profile:instance、#id、:-1、:0、純粋なプロファイル名、または次のような形式の co.haply.hub::*:0 の profile 名称ワイルドカード パターン。このチュートリアルでは、この文字列をそのまま転送し、解析はサーバー側で行います。
- Python
- C++ (nlohmann)
- C++ (Glaze)
def discover_session(session_arg):
global session_selector
if session_arg:
# Direct lookup (e.g. ":my_profile:0", "#42", ":-1")
if api_get(f"/sessions/{session_arg}") is None:
return False
session_selector = session_arg
return True
# Otherwise: list and pick
data = api_get("/sessions")
sessions = data.get("data", {}).get("sessions", [])
for i, s in enumerate(sessions):
name = s.get("config", {}).get("profile", {}).get("name", "default")
print(f" [{i}] session #{s['session_id']} profile={name}")
picked = sessions[int(input("Pick session index: "))]
name = picked.get("config", {}).get("profile", {}).get("name", "")
# Prefer the profile selector — it survives restarts; id doesn't
session_selector = (f":{name}:0" if name and name != "default"
else f"#{picked['session_id']}")
return True
static bool discover_session(const std::string &session_arg) {
if (!session_arg.empty()) {
const auto data = http_get(BASE_URL + "/sessions/" + session_arg);
if (data.is_null()) return false;
session_selector = session_arg;
return true;
}
const auto data = http_get(BASE_URL + "/sessions");
const json &list = data["data"]["sessions"];
for (size_t i = 0; i < list.size(); ++i) {
const int sid = list[i].value("session_id", 0);
std::string prof = "default";
if (list[i].contains("config") && list[i]["config"].contains("profile"))
prof = list[i]["config"]["profile"].value("name", std::string{"default"});
printf(" [%zu] session #%d profile=%s\n", i, sid, prof.c_str());
}
std::string line; std::getline(std::cin, line);
const json &picked = list[std::stoi(line)];
std::string prof;
if (picked.contains("config") && picked["config"].contains("profile"))
prof = picked["config"]["profile"].value("name", std::string{});
session_selector = (!prof.empty() && prof != "default")
? ":" + prof + ":0"
: "#" + std::to_string(picked.value("session_id", 0));
return true;
}
構造体を使用して応答構造をモデル化します。Glaze はその構造に対して自動的に反射処理を行います:
struct profile_info { std::string name; };
struct session_config{ std::optional<profile_info> profile; };
struct session_info { int session_id{}; std::optional<session_config> config; };
struct sessions_list { int session_count{}; std::vector<session_info> sessions; };
static bool discover_session(const std::string &session_arg) {
if (!session_arg.empty()) {
auto resp = requests::get((BASE_URL + "/sessions/" + session_arg).c_str());
if (!resp || resp->status_code != 200) return false;
session_selector = session_arg;
return true;
}
auto list = http_get_envelope<sessions_list>(BASE_URL + "/sessions");
if (!list || list->sessions.empty()) return false;
for (size_t i = 0; i < list->sessions.size(); ++i) {
const auto &s = list->sessions[i];
std::string prof = "default";
if (s.config && s.config->profile) prof = s.config->profile->name;
printf(" [%zu] session #%d profile=%s\n", i, s.session_id, prof.c_str());
}
std::string line; std::getline(std::cin, line);
const auto &picked = list->sessions[std::atoi(line.c_str())];
std::string prof;
if (picked.config && picked.config->profile) prof = picked.config->profile->name;
session_selector = (!prof.empty() && prof != "default")
? ":" + prof + ":0"
: "#" + std::to_string(picked.session_id);
return true;
}
デバイスセレクター — *inverse/0
各設定呼び出しは、特定のデバイスに限定されます。このチュートリアルでは、シリーズワイルドカードとインデックスを組み合わせた形式のセレクタを使用しています:
/*inverse/0/config/<key>
*inverse「Inverse」シリーズの任意のデバイスとペアリング(inverse3、inverse3x、minverse) — 具体的なモデルがどれであっても、このチュートリアルは修正なしで実行できます。0このシリーズでは、インデックスは 0 から数えられます。なお、このチュートリアルでは、最初の Inverse デバイスについてのみ操作を行います。
対象デバイスを変更するには、1つの文字列を変更するだけで済みます — 渡す --device パラメータを指定するだけで済みます(パスはこのパラメータから生成されます):
/verse_grip/0/config/basis?session=... # --device verse_grip/0
/*verse_grip/0/config/basis?session=... # --device '*verse_grip/0' — first grip, any kind
/inverse3/A14/config/mount?session=... # --device 'inverse3/#A14'
/*inverse:left/0/config/basis?session=... # --device '*inverse:left/0' — first left-handed inverse
/*inverse:right/0/config/basis?session=... # --device '*inverse:right/0' — first right-handed inverse
:left / :right インデックス内のクラス以前発効――「――」*inverse:left/0「第 0 個」と表示左手 「inverse」であり、「0番目の inverse(できれば左側)」ではない。参照: セレクタ――クラスフィルターはインデックス作成前に解析される。
チュートリアルは起動時に GET …/config/basis リクエスト。セッション内に
--device 対応するデバイス(例:右利き用デバイスのみがある環境での使用など) --device '*inverse:left/0'),サービスは以下を返します
404 no device matches selector '<sel>';チュートリアルは印刷されます Device '<sel>' not found in session '<sel>'.
さらに、状態を 1 終了。古いデフォルト値でキーボードループに入ることはありません。
完全な構文については、以下を参照してください。 セレクタ――デバイスセレクタ。ハードコーディングではなく、デバイス選択メニューを構築するには、以下を使用してください。 GET /devices?session=<selector>(チュートリアル 00)デバイスを列挙し、選択した device_id 配置パスを指定してください。
POST 配置 — basis、preset、mount
3つのリクエストの構造は同じですが、リクエスト本文の構造は異なります。各POSTリクエストでは、以下が返されます。 200,そして data 結果値が含まれています。セッションまたはデバイスセレクタがどのオブジェクトにも一致しなかった場合は、 404。
基礎
POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json
{"permutation": "XZY"}
回答:{"ok": true, "data": {"permutation": "XZY"}}
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_basis():
perm, _ = BASIS_OPTIONS[basis_index]
api_post(session_url("/inverse3/0/config/basis"), {"permutation": perm})
static void post_basis() {
http_post_json(session_url("/inverse3/0/config/basis"),
{{"permutation", BASIS_OPTIONS[basis_index].first}});
}
struct basis_body { std::string permutation; };
static void post_basis() {
http_post_json(session_url("/inverse3/0/config/basis"),
basis_body{BASIS_OPTIONS[basis_index].first});
}
プリセット
POST /*inverse/0/config/preset?session=:my_profile:0
Content-Type: application/json
{"preset": "arm_front_centered"}
回答:{"ok": true, "data": {"preset": "arm_front_centered"}}
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_preset():
preset = PRESET_OPTIONS[preset_index]
api_post(session_url("/inverse3/0/config/preset"), {"preset": preset})
static void post_preset() {
http_post_json(session_url("/inverse3/0/config/preset"),
{{"preset", PRESET_OPTIONS[preset_index]}});
}
struct preset_body { std::string preset; };
static void post_preset() {
http_post_json(session_url("/inverse3/0/config/preset"),
preset_body{PRESET_OPTIONS[preset_index]});
}
マウント
POST /*inverse/0/config/mount?session=:my_profile:0
Content-Type: application/json
{
"transform": {
"position": {"x": 0.02, "y": 0.0, "z": 0.0},
"rotation": {"w": 0.966, "x": 0.0, "y": 0.259, "z": 0.0},
"scale": {"x": 1.0, "y": 1.0, "z": 1.0}
}
}
回答:{"ok": true, "data": {"transform": { ... }}} — エコーバックの正規化後に有効になる変換。
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_mount():
body = {
"transform": {
"position": {"x": mount_pos[0], "y": mount_pos[1], "z": mount_pos[2]},
"rotation": quat_from_euler_deg(*mount_rot),
"scale": {"x": mount_scale[0], "y": mount_scale[1], "z": mount_scale[2]},
}
}
api_post(session_url("/inverse3/0/config/mount"), body)
static void post_mount() {
http_post_json(session_url("/inverse3/0/config/mount"), {
{"transform", {
{"position", {{"x", mount_pos[0]}, {"y", mount_pos[1]}, {"z", mount_pos[2]}}},
{"rotation", quat_from_euler_deg(mount_rot[0], mount_rot[1], mount_rot[2])},
{"scale", {{"x", mount_scale[0]}, {"y", mount_scale[1]}, {"z", mount_scale[2]}}},
}},
});
}
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.0f, 1.0f, 1.0f}; };
struct mount_body { transform_t transform; };
static void post_mount() {
http_post_json(session_url("/inverse3/0/config/mount"), mount_body{
transform_t{
.position = vec3{mount_pos[0], mount_pos[1], mount_pos[2]},
.rotation = quat_from_euler_deg(mount_rot[0], mount_rot[1], mount_rot[2]),
.scale = vec3{mount_scale[0], mount_scale[1], mount_scale[2]},
}});
}
mount と preset 排他そのうちの1つに対してPOSTを実行すると、デバイス上でもう1つがクリアされます。このチュートリアルでは、この点については明示的に追跡していません。各POSTは独立しており、競合の解決はサーバー側が行います。同じルールについてWebSocket側の観点から説明した内容については、チュートリアル07を参照してください。
DELETE リセット — 3回目の呼び出し
reset 各設定項目に対して 1 回ずつ DELETE リクエストを送信します。その都度、 200,そして data これには、復元後のデフォルト値が含まれています。
- Python
- C++ (nlohmann)
- C++ (Glaze)
def reset_all():
api_delete(session_url("/inverse3/0/config/basis"))
api_delete(session_url("/inverse3/0/config/preset"))
api_delete(session_url("/inverse3/0/config/mount"))
static void reset_all() {
http_delete(session_url("/inverse3/0/config/basis"));
http_delete(session_url("/inverse3/0/config/preset"));
http_delete(session_url("/inverse3/0/config/mount"));
}
static void reset_all() {
http_delete(session_url("/inverse3/0/config/basis"));
http_delete(session_url("/inverse3/0/config/preset"));
http_delete(session_url("/inverse3/0/config/mount"));
}
組み合わせマウント・回転
転送時には、transform.rotation 単位四元数です。このチュートリアルでは、回転を Z-Y-X の内旋オイラー角の3元組(X軸周りのピッチ、Z軸周りのヨー、Y軸周りのロール。いずれも単位は度)として保存し、POSTのたびに四元数として再構成します。
- Python
- C++ (nlohmann)
- C++ (Glaze)
def quat_from_euler_deg(pitch_x, yaw_z, roll_y):
"""Hamilton quaternion for q = q_z * q_y * q_x (apply X, then Y, then Z)."""
hx, hy, hz = (math.radians(a) * 0.5 for a in (pitch_x, roll_y, yaw_z))
cx, sx = math.cos(hx), math.sin(hx)
cy, sy = math.cos(hy), math.sin(hy)
cz, sz = math.cos(hz), math.sin(hz)
return {
"w": cz*cy*cx + sz*sy*sx,
"x": cz*cy*sx - sz*sy*cx,
"y": cz*sy*cx + sz*cy*sx,
"z": sz*cy*cx - cz*sy*sx,
}
static json quat_from_euler_deg(float pitch_x, float yaw_z, float roll_y) {
constexpr float deg_to_rad = 3.14159265358979323846f / 180.0f;
const float hx = pitch_x * 0.5f * deg_to_rad;
const float hy = roll_y * 0.5f * deg_to_rad;
const float hz = yaw_z * 0.5f * deg_to_rad;
const float cx = std::cos(hx), sx = std::sin(hx);
const float cy = std::cos(hy), sy = std::sin(hy);
const float cz = std::cos(hz), sz = std::sin(hz);
return {
{"w", cz * cy * cx + sz * sy * sx},
{"x", cz * cy * sx - sz * sy * cx},
{"y", cz * sy * cx + sz * cy * sx},
{"z", sz * cy * cx - cz * sy * sx},
};
}
static quat quat_from_euler_deg(float pitch_x, float yaw_z, float roll_y) {
constexpr float deg_to_rad = 3.14159265358979323846f / 180.0f;
const float hx = pitch_x * 0.5f * deg_to_rad;
const float hy = roll_y * 0.5f * deg_to_rad;
const float hz = yaw_z * 0.5f * deg_to_rad;
const float cx = std::cos(hx), sx = std::sin(hx);
const float cy = std::cos(hy), sy = std::sin(hy);
const float cz = std::cos(hz), sz = std::sin(hz);
return quat{
.w = cz * cy * cx + sz * sy * sx,
.x = cz * cy * sx - sz * sy * cx,
.y = cz * sy * cx + sz * cy * sx,
.z = sz * cy * cx - cz * sy * sx,
};
}
単位ハミルトン四元数、右手座標系、スカラーが先(w)——本サービスの他の部分における規定と同様に、以下を参照のこと。 quaternion。組み合わせの順序は次のとおりです。 Z-Y-X 内旋(固有)(q = q_z * q_y * q_x):まず X 軸を中心にピッチ(俯仰)を与え、次に Y 軸を中心にロール(横転)を与え、最後に Z 軸を中心にヨー(偏航)を与える。
このチュートリアルでは、各行の状態について、導出された四元数とオイラー角の3元組を同時に出力します。これにより、デバイスを回転させる前に、組み合わせの結果を確認することができます。セッションにすでに値が存在する場合でも、ローカルのオイラー角の状態は (0, 0, 0) はじめに――初めて mount POST 元の値を上書きします。
入力モデル(概要)
重要なのはHTTPの接続ロジックであり、キーボード操作はあくまで副次的なものです。ここには、意図的に簡略化された点が2つあります:
- Python 使用
keyboard包——クロスプラットフォーム対応、長押しによる繰り返し操作をネイティブで処理。方向キー、Page Up/Page Downおよび=/-押している間、マウント軸が徐々に移動します;B和P立ち上がりエッジで、ベースとプリセットを切り替えます。 - C++ 使用
std::getline(std::cin, ...)コンパクトなトークン構文(x+20、sx-5、u+10)——連続的な微調整にはあまり使い勝手が良くないが、わざわざ……を使う必要はない#ifdef各プラットフォームのコンソールAPIを区別しているため、移植が容易になります。
ソースコード
チュートリアル 08 も SDK とともにローカルにインストールされます。サービスインストールディレクトリ内の tutorials/08-haply-inverse-http-remote-config/ その中で見つけた。
関連項目: セッション — リモートコントロール·セレクタ·デバイス設定·ベースの置換·マウントとワークスペース·JSON 規約·チュートリアル 00 — デバイス一覧·チュートリアル 07 — ベースとマウント(WebSocket 版)