08. Remote-Session-Konfigurator
Konfigurieren Sie eine bereits anderswo laufende Session neu – etwa eine andere App, eine Szene in Unity oder eine Demo von Haply Hub –, indem Sie per HTTP REST-Aufrufe gegen deren Gerät absetzen. Dieses Tutorial öffnet keine WebSocket: Es werden nur GET-, POST- und DELETE-Anfragen gesendet, um Basis, Workspace-Preset oder Mount-Transformation zu ändern, während die andere App weiterhin Haptik ausgibt.
Anwendungsfälle
- Laufende Demo live anpassen. Starten Sie die Orb-Demo in Haply Hub und führen Sie dann dieses Tutorial in einem zweiten Terminal aus, um die Basis-Permutation zu tauschen, das Workspace-Preset zu ändern oder die Mount-Transformation leicht zu verschieben – das Koordinatensystem der Orb-Szene verschiebt sich sofort, ohne dass die Demo gestoppt werden muss.
- Workspace-Kalibrierung pro Benutzer. Lassen Sie die haptische Szene auf dem Hauptrechner laufen und ermöglichen Sie einem Bediener im selben Netzwerk, einen
mount-Offset / eine Rotation / Skalierung zu übermitteln, damit der virtuelle Workspace mit dem Schreibtisch des Benutzers übereinstimmt. - Optionsmenü mit Geräteauswahl. Dieselben HTTP-Helfer können
GET /devicesabfragen (siehe Tutorial 00), um Geräte aufzulisten und ein interaktives Menü aufzubauen – ein Gerät auswählen und anschließend neu konfigurieren –, ohne das WebSocket der Session anzutasten. Das Tutorial fragt/sessionsab und hartkodiert*inverse/0; der Wechsel zu einer über/devicesgesteuerten Auswahl ist jedoch eine lokale Änderung. - Skriptgesteuerte Neukonfiguration. Automatisieren Sie Vorabschritte (Basis, Preset und Mount setzen), bevor eine Session mit der Aufzeichnung beginnt, ohne die Einrichtung in jedem Client einzubetten.
Voraussetzungen
Tutorial 08 konfiguriert eine bereits laufende Session neu. Sie benötigen eine beliebige aktive haptische Session – ein anderes Tutorial, eine Szene in Unity oder eine Demo in Haply Hub.
Öffnen Sie Haply Hub, starten Sie die Orb-Demo und sprechen Sie sie anschließend direkt an:
./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"
Die Orb-Szene rendert eine Kugel im Workspace des Geräts. Wenn Sie mit Tutorial 08 Basis oder Preset durchschalten oder die Mount-Transformation leicht verschieben, verschiebt sich das Koordinatensystem der Orb-Szene sichtbar in Echtzeit.
Verwendung
# 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
Das Tutorial zeigt beim Start die aktuelle Basis / das Preset / den Mount der Sitzung an und wartet dann auf Tastendrücke — jeder Tastendruck sendet genau einen REST-Aufruf.
Sitzungen ohne Profilnamen können nur über eine numerische ID angesprochen werden — und diese ändert sich bei jedem Lauf. Lassen Sie Ihre Hauptanwendung bei ihrer ersten Nachricht session.configure.profile.name aufrufen; danach können Sie einen stabilen Selektor wie --session :my_profile:0 über alle Läufe hinweg wiederverwenden. Siehe Sitzungen — Profilname.
Tastenbelegung
- Python
- C++
| Taste | Aktion |
|---|---|
B | Basis-Permutation durchschalten |
P | Workspace-Preset durchschalten |
W / E / R | Mount-Bearbeitungsmodus wählen — Position (mm) / Rotation (°) / Skalierung (%) |
← / → | Im aktuellen Modus einen Schritt −X / +X |
↑ / ↓ | Im aktuellen Modus einen Schritt +Y / −Y |
Page Up / Page Down | Im aktuellen Modus einen Schritt +Z / −Z |
= / - | Gleichmäßige Skalierung ± auf allen drei Achsen gleichzeitig (immer verfügbar) |
Delete | DELETE für Basis + Preset + Mount — Zurücksetzen auf die Gerätestandards |
H | Hilfe anzeigen |
Esc | Beenden (Ctrl+C funktioniert ebenfalls) |
Zeilenbasiert — Befehl eingeben und dann ENTER drücken.
| Befehl | Aktion |
|---|---|
b | Basis-Permutation durchschalten |
p | Workspace-Preset durchschalten |
w / e / r | Mount-Bearbeitungsmodus wählen — Position (mm) / Rotation (°) / Skalierung (%) |
x+[N] … z-[N] | Die aktuelle Achse um N in der natürlichen Einheit des aktiven Modus verschieben (x+ ohne Wert = Standard 5) |
sx+[N] … sz-[N] | Kurzbefehl für ungleichmäßige Skalierung auf einer Achse (Prozent), immer verfügbar |
u+[N] / u-[N] | Gleichmäßige Skalierung ± N % auf allen drei Achsen gleichzeitig |
reset | DELETE für Basis + Preset + Mount |
h | Hilfe anzeigen |
Zum Beenden Strg+C drücken (oder Strg+D / EOF).
HTTP-Methoden — GET, POST, DELETE
Das Tutorial verwendet drei HTTP-Methoden und nur diese drei. Jeder Aufruf liefert den standardmäßigen JSON-Envelope ({"ok": true, "data": {...}} bei Erfolg, {"ok": false, "error": "..."} bei Fehler) sowie einen von drei Statuscodes: 200 Erfolg, 400 fehlerhafte Anfrage, 404 Selektor hat nichts gefunden.
| Methode | Zweck | Verwendete Pfade |
|---|---|---|
GET | Aktuellen Zustand lesen — Sitzungsliste, gezielte Sitzungsabfrage, aktuelle Konfigurationswerte | /sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=... |
POST | Einen Konfigurationswert ersetzen — Request-Body ist JSON | /<device_selector>/config/{basis,preset,mount}?session=... |
DELETE | Einen Konfigurationswert auf den Gerätestandard zurücksetzen | /<device_selector>/config/{basis,preset,mount}?session=... |
HTTP-Hilfsfunktionen
Ein dünner Wrapper um die drei Methoden, damit sich der Rest des Tutorials wie reine Anwendungslogik liest:
- Python
- C++ (nlohmann)
- C++ (Glaze)
Python verwendet requests.Session() für HTTP-Keep-Alive (verkürzt die Latenz pro Anfrage von ~50 ms auf ~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 stellt requests::get / requests::post / requests::Delete bereit (großes D — delete ist ein C++-Schlüsselwort). Für POST ist eine manuell erstellte Anfrage nötig, um Content-Type: application/json zu setzen:
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;
}
Response-Bodies haben immer den {"ok", "data": T}-Envelope. Ein einziges Template umschließt jeden typisierten GET; dasselbe HttpRequest-Muster bedient POSTs mit glz::write_json:
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;
}
Sitzungsermittlung — GET /sessions
Verzweigt anhand von --session:
--session SELECTORangegeben → einGET /sessions/<SELECTOR>.200→ verwenden;404→ Fehler ausgeben.- Kein Flag →
GET /sessions(Auflistung) → Sitzungen mit Profilnamen anzeigen → nach einem Index fragen → den endgültigen Selektor erstellen (bevorzugt:profile:0, wenn verfügbar; andernfalls#id).
SELECTOR akzeptiert jede Form, die unter Selektoren — Sitzungsselektor definiert ist: :profile:instance, #id, :-1, :0, einen einfachen Profilnamen oder ein Profilnamen-Wildcard-Muster wie co.haply.hub::*:0. Das Tutorial übergibt die Zeichenkette unverändert; der Dienst parst sie.
- 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;
}
Bildet die Antwortstruktur als Structs ab; Glaze reflektiert sie automatisch:
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;
}
Geräte-Selektor — *inverse/0
Jeder Konfigurationsaufruf ist auf ein Gerät beschränkt. Das Tutorial verwendet einen Selektor aus Familien-Wildcard + Index:
/*inverse/0/config/<key>
*inversepasst auf jedes Gerät der Inverse-Familie (inverse3,inverse3x,minverse) — das Tutorial funktioniert unverändert, unabhängig vom konkreten Modell.0ist der nullbasierte Index innerhalb dieser Familie — das Tutorial spricht immer nur das erste Inverse an.
Das Umstellen auf ein anderes Gerät erfordert nur eine Zeichenkettenänderung — übergeben Sie --device (der Pfad wird daraus erstellt):
/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
Die Klasse :left / :right wird vor der Indizierung angewendet — *inverse:left/0 bedeutet „die 0-te linkshändige Inverse“, nicht „die 0-te Inverse, hoffentlich links“. Siehe Selektoren — Klassenfilter werden vor der Indizierung aufgelöst.
Das Tutorial stellt beim Start eine GET …/config/basis-Anfrage. Hat die Sitzung kein
Gerät, das --device entspricht (z. B. --device '*inverse:left/0' bei einem
nur rechtshändigen Aufbau), antwortet der Dienst mit 404 no device matches selector '<sel>'; das Tutorial gibt Device '<sel>' not found in session '<sel>'. aus
und beendet sich mit dem Status 1. Dabei wird keine Tastaturschleife mit veralteten Standardwerten gestartet.
Die vollständige Syntax finden Sie unter Selektoren — Gerätselektor. Um ein Geräte-Auswahlmenü statt fest codierter Werte zu erstellen, zählen Sie die Geräte mit GET /devices?session=<selector> auf (Tutorial 00) und übernehmen die gewählte device_id in die Konfigurationspfade.
POST-Konfiguration — basis, preset, mount
Drei Schlüssel mit gleichem Anfrageaufbau, aber unterschiedlichem Body-Schema. Jeder POST liefert ein 200 mit dem resultierenden Wert in data, oder ein 404, wenn Sitzungs- oder Gerätselektor keinen Treffer ergab.
Basis
POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json
{"permutation": "XZY"}
Antwort: {"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});
}
Preset
POST /*inverse/0/config/preset?session=:my_profile:0
Content-Type: application/json
{"preset": "arm_front_centered"}
Antwort: {"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]});
}
Mount
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}
}
}
Antwort: {"ok": true, "data": {"transform": { ... }}} — gibt die effektive Transformation nach der Normalisierung zurück.
- 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 und preset schließen sich gegenseitig ausEin POST für eines der beiden Felder löscht auf dem Gerät das jeweils andere. Das Tutorial verfolgt dies nicht explizit — jeder POST ist eigenständig, und der Server löst den Konflikt auf. Siehe Tutorial 07 für dieselbe Regel aus Sicht von WebSocket.
DELETE-Zurücksetzung — drei Aufrufe
reset sendet pro Konfigurationsschlüssel einen DELETE. Jeder liefert 200 mit dem nun standardmäßigen Wert in 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"));
}
Zusammensetzen der Mount-Rotation
transform.rotation ist in der Übertragung ein Einheitsquaternion. Das Tutorial speichert die Rotation als intrinsisches Euler-Tripel in der Reihenfolge Z-Y-X (Nicken um X, Gieren um Z, Rollen um Y — alle in Grad) und setzt das Quaternion bei jedem POST neu zusammen.
- 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,
};
}
Einheitsquaternion nach Hamilton, rechtshändig, skalar-zuerst (w) — dieselbe Konvention wie im restlichen Dienst, siehe quaternion. Die Kompositionsreihenfolge ist Z-Y-X intrinsisch (q = q_z * q_y * q_x): Wenden Sie zuerst die Drehung um X (Pitch) an, danach die Drehung um Y (Roll) und zuletzt die Drehung um Z (Yaw).
Das Tutorial gibt bei jeder Statuszeile das abgeleitete Quaternion zusammen mit dem Euler-Tripel aus, damit Sie die Komposition überprüfen können, bevor sich das Gerät dreht. Der lokale Euler-Zustand beginnt bei (0, 0, 0), unabhängig davon, was die Session bereits enthält — der erste mount POST überschreibt den vorhandenen Wert.
Eingabemodell (kurz)
Die Anbindung von HTTP steht im Mittelpunkt; die Tastatursteuerung ist zweitrangig. Zwei bewusste Abkürzungen:
- Python verwendet das Paket
keyboard— plattformübergreifend und mit nativer Behandlung von Tastenwiederholungen beim Halten. Pfeiltasten,Page Up/Page Downsowie=/-verschieben die Mount-Achsen, solange die Taste gedrückt wird;BundPwechseln bei der ansteigenden Flanke zyklisch durch Basis und Preset. - C++ verwendet
std::getline(std::cin, ...)und eine kompakte Token-Grammatik (x+20,sx-5,u+10) — für fortlaufende Feinanpassungen weniger komfortabel, dafür ohne plattformspezifische#ifdef-Verzweigungen für Konsolen-APIs portierbar.
Quellcode
Tutorial 08 wird auch lokal mit dem SDK installiert — suchen Sie im Installationsverzeichnis des Dienstes unter tutorials/08-haply-inverse-http-remote-config/.
Verwandt: Sessions — Fernsteuerung · Selektoren · Gerätekonfiguration · Basis-Permutation · Mount & Workspace · JSON-Konventionen · Tutorial 00 — Geräteliste · Tutorial 07 — Basis & Mount (Version WebSocket)