Quick start
Five runnable scenarios: raw pose, Skeleton, and the three dexterous hand mappings.
Quick start
This chapter contains only code you can run. Each scenario maps to a standalone example file in
examples/glove/python/src/.
They do not depend on each other, so run only the one you need. Every example, with the main API each
file shows, is in Example index.
Before you start, make sure the SDK is installed and verified as described in Installation & requirements, and that a glove is connected. Every example stops with Ctrl+C.
Where to start: Run quick_start.py first to confirm that pose frames arrive continuously. For SDK-processed skeleton data, run skeleton.py. Once an algorithm package is installed, run foreground_runtime.py for dexterous hand output. Commands and expected output for each are below.
Read raw pose data
Pose comes straight from the device, without SDK processing and without an algorithm package:
File: quick_start.py
Zero-argument configuration that auto-discovers the single supported glove and receives pose frames through a callback. This is the shortest path:
from __future__ import annotations
from qnbot_sdk import Sample, Sdk
from qnbot_sdk.glove import GloveConfig, GlovePose
def print_pose(sample: Sample[GlovePose]) -> None:
print(
f"pose sequence={sample.sequence} "
f"fingertips={sample.value.payload.fingertip_local}"
)
def main() -> None:
sdk = Sdk(devices=(GloveConfig(),))
glove = sdk.glove()
pose = glove.device().pose()
pose.subscribe(print_pose)
glove.start()
print("running; press Ctrl+C to stop", flush=True)
glove.run_forever()
if __name__ == "__main__":
main()Run:
python examples/glove/python/src/quick_start.pyExpected output (the SDK first prints two connection log lines, sdk_initialized and device_connected; this is the example's own output, with pose values elided):
running; press Ctrl+C to stop
pose sequence=1 fingertips={<GloveFinger.INDEX: 'index'>: GloveNodePose(position=(…), quaternion_xyzw=(…)), …}
pose sequence=2 fingertips={…}The process keeps running and prints continuously. If multiple gloves are found, Sdk(...) fails immediately; select one with port or serial_number, see Device discovery & connection.
Skeleton
File: skeleton.py
Adds SDK-processed data on top of pose reading: subscribes to the OpenXR skeleton and reads joint angles and bone poses. Skeleton is provided by the SDK and needs no algorithm package; the first run needs one capture calibration, which later runs reuse. See Reading retargeting data for the definition.
Keep the SDK scheduling advancing as described in Runtime modes.
from __future__ import annotations
import argparse
from qnbot_sdk import Sample, Sdk, SerialConnection, Side
from qnbot_sdk.glove import GloveConfig, HandJointCommand, HandSkeletonPose
def print_joint_angles(sample: Sample[HandJointCommand]) -> None:
print(
f"skeleton sequence={sample.sequence} "
f"target={sample.value.target} joints={sample.value.joints}"
)
def print_pose(sample: Sample[HandSkeletonPose]) -> None:
wrist = sample.value.positions_local[0]
index_tip = sample.value.positions_local[9]
print(
f"pose sequence={sample.sequence} frame={sample.value.coordinate_frame} "
f"wrist={wrist} index_tip={index_tip}"
)
def main() -> None:
arguments = argparse.ArgumentParser(description="Read SDK-owned skeleton output")
arguments.add_argument("--port", required=True, help="Serial port for the glove")
arguments.add_argument(
"--side",
choices=(Side.LEFT.value, Side.RIGHT.value),
required=True,
help="Physical glove side",
)
options = arguments.parse_args()
sdk = Sdk(
devices=(
GloveConfig(
side=Side(options.side),
connection=SerialConnection(port=options.port),
),
)
)
glove = sdk.glove()
skeleton = glove.device().skeleton()
skeleton.joint_angles().subscribe(print_joint_angles)
skeleton.pose().subscribe(print_pose)
glove.start()
print("running; press Ctrl+C to stop", flush=True)
glove.run_forever()
if __name__ == "__main__":
main()Run:
python examples/glove/python/src/skeleton.py --port <serial-port> --side rightExpected output (joint values and coordinates elided):
running; press Ctrl+C to stop
skeleton sequence=1 target=hand.skeleton.right joints={…}
pose sequence=1 frame=wrist_local wrist=(0.0, 0.0, 0.0) index_tip=(…)Both lines print once per frame: skeleton carries the joint angles, pose the bone node poses.
Dexterous hand output
The three mappings differ only in how gloves map to dexterous hands:
| Scenario | Mapping | Example |
|---|---|---|
| One-to-one | One glove drives one named target | foreground_runtime.py |
| One-to-many | One glove drives several dexterous hands | multiple_outputs.py |
| Many-to-many | Several gloves drive several dexterous hands | device_lifecycle.py |
All three mappings need an algorithm package installed first, see Reading retargeting data — these values are the retargeting result the package computes for each target. The first run also needs one capture calibration, which the SDK guides through using the package's prompts and later runs reuse.
One-to-one: one glove drives one named target
The program runs continuously in the current thread and receives the target output through a callback.
from __future__ import annotations
import argparse
from qnbot_sdk import (
DeviceSelector,
Health,
Sample,
Sdk,
SerialConnection,
Side,
TargetAlgorithm,
TargetConfig,
)
from qnbot_sdk.glove import GloveConfig, HandJointCommand
DEFAULT_TARGET_NAME = "openxr_hand"
def create_sdk(port: str, side: Side, target_name: str, package_id: str) -> Sdk:
source = DeviceSelector(type="glove", name="primary")
return Sdk(
devices=(
GloveConfig(
name="primary",
side=side,
connection=SerialConnection(port=port),
),
),
targets=(
TargetConfig(
name=target_name,
side=side,
source=source,
algorithms=(TargetAlgorithm(id=package_id),),
),
),
)
def print_output(sample: Sample[HandJointCommand]) -> None:
print(
f"retargeting sequence={sample.sequence} "
f"target={sample.value.target} joints={sample.value.joints}"
)
def print_health(label: str, health: Health) -> None:
devices = ", ".join(
f"{source_id}(connected={device.connected}, stale={device.stale})"
for source_id, device in health.devices.items()
)
print(
f"{label} health ok={health.ok} warnings={health.warning_count} "
f"errors={health.error_count} devices=[{devices}]"
)
def main() -> None:
arguments = argparse.ArgumentParser(
description="Run the Glove domain on the current thread"
)
arguments.add_argument("--port", required=True, help="Serial port for the glove")
arguments.add_argument(
"--side",
choices=(Side.LEFT.value, Side.RIGHT.value),
required=True,
help="Physical glove side",
)
arguments.add_argument("--target-name", default=DEFAULT_TARGET_NAME)
arguments.add_argument("--package-id", required=True)
options = arguments.parse_args()
sdk = create_sdk(
options.port,
Side(options.side),
options.target_name,
options.package_id,
)
glove = sdk.glove()
output = glove.device().output(name=options.target_name)
output.subscribe(print_output)
glove.start()
print_health("running", glove.health())
print("running; press Ctrl+C to stop", flush=True)
glove.run_forever()
if __name__ == "__main__":
main()Run:
python examples/glove/python/src/foreground_runtime.py \
--port <serial-port> --side right --package-id <package-id>Expected output (joint values elided):
running health ok=True warnings=0 errors=0 devices=[glove.<SN>.right(connected=True, stale=False)]
running; press Ctrl+C to stop
retargeting sequence=1 target=hand.openxr_hand.right joints={…}If no output arrives, check the algorithm package ID, the target configuration and the calibration status, see Connected but no data.
One-to-many: one glove drives several dexterous hands
Configures several targets for the same source and reads each output by name. The glove connection and data stream stay single.
from __future__ import annotations
import argparse
from collections.abc import Callable
from qnbot_sdk import (
DeviceSelector,
Sample,
Sdk,
SerialConnection,
Side,
TargetAlgorithm,
TargetConfig,
)
from qnbot_sdk.glove import GloveConfig, HandJointCommand
def create_sdk(
port: str,
side: Side,
package_id: str,
) -> Sdk:
source = DeviceSelector(type="glove", name="primary")
return Sdk(
devices=(
GloveConfig(
name="primary",
side=side,
connection=SerialConnection(port=port),
),
),
targets=(
TargetConfig(
name="primary",
side=side,
source=source,
algorithms=(TargetAlgorithm(id=package_id),),
),
TargetConfig(
name="backup",
side=side,
source=source,
algorithms=(TargetAlgorithm(id=package_id),),
),
),
)
def print_output(name: str) -> Callable[[Sample[HandJointCommand]], None]:
def show(sample: Sample[HandJointCommand]) -> None:
print(
f"callback {name} sequence={sample.sequence} "
f"target={sample.value.target} joints={sample.value.joints}"
)
return show
def main() -> None:
arguments = argparse.ArgumentParser(
description="Read multiple hand outputs driven by one glove"
)
arguments.add_argument("--port", required=True, help="Serial port for the glove")
arguments.add_argument(
"--side",
choices=(Side.LEFT.value, Side.RIGHT.value),
required=True,
help="Physical glove side",
)
arguments.add_argument(
"--package-id",
required=True,
help="Installed algorithm package ID used by both output instances",
)
options = arguments.parse_args()
sdk = create_sdk(
options.port,
Side(options.side),
options.package_id,
)
glove = sdk.glove()
device = glove.device()
primary = device.output(name="primary")
backup = device.output(name="backup")
primary.subscribe(print_output("primary"))
backup.subscribe(print_output("backup"))
glove.start()
print("running; press Ctrl+C to stop", flush=True)
glove.run_forever()
if __name__ == "__main__":
main()Run:
python examples/glove/python/src/multiple_outputs.py \
--port <serial-port> --side right --package-id <package-id>Expected output (each target's output carries its own name; joint values elided):
running; press Ctrl+C to stop
callback primary sequence=1 target=hand.primary.right joints={…}
callback primary sequence=2 target=hand.primary.right joints={…}
…
callback backup sequence=1 target=hand.backup.right joints={…}For the full target configuration, see Reading retargeting data.
Many-to-many: several gloves drive several dexterous hands
Each glove gets its own target; the program selects each device and reads its own output. The script first waits for both outputs, stops the right device to confirm the left keeps advancing, then restarts it.
from __future__ import annotations
import argparse
import time
from qnbot_sdk import (
AlgorithmsConfig,
CaptureConfig,
CaptureControl,
CaptureInteractionMode,
DeviceSelector,
ReadChannel,
Sample,
Sdk,
SerialConnection,
Side,
TargetAlgorithm,
TargetConfig,
)
from qnbot_sdk.glove import (
CalibrationJobState,
CalibrationProgress,
CaptureProgress,
CaptureSessionState,
CaptureStageState,
GloveConfig,
HandJointCommand,
)
DEFAULT_TARGET_NAME = "openxr_hand"
def create_sdk(
left_port: str,
right_port: str,
target_name: str,
package_id: str,
) -> Sdk:
return Sdk(
devices=(
GloveConfig(
name="left",
side=Side.LEFT,
connection=SerialConnection(port=left_port),
),
GloveConfig(
name="right",
side=Side.RIGHT,
connection=SerialConnection(port=right_port),
),
),
targets=(
TargetConfig(
name=target_name,
side=Side.LEFT,
source=DeviceSelector(type="glove", name="left"),
algorithms=(TargetAlgorithm(id=package_id),),
),
TargetConfig(
name=target_name,
side=Side.RIGHT,
source=DeviceSelector(type="glove", name="right"),
algorithms=(TargetAlgorithm(id=package_id),),
),
),
algorithms=AlgorithmsConfig(
capture=CaptureConfig(
interaction=CaptureInteractionMode.EXTERNAL,
),
),
)
def print_output(device_name: str, sample: Sample[HandJointCommand]) -> None:
print(
f"{device_name} retargeting sequence={sample.sequence} "
f"target={sample.value.target} joints={sample.value.joints}"
)
def handle_capture_prompt(
capture_progress: ReadChannel[CaptureProgress],
capture_control: CaptureControl,
calibration_progress: ReadChannel[CalibrationProgress],
handled_request_ids: set[str],
) -> None:
capture = capture_progress.latest()
if capture is not None:
value = capture.value
if value.session_state is CaptureSessionState.FAILED:
message = value.failure.message if value.failure else "unknown error"
raise RuntimeError(f"capture failed: {message}")
stage = value.stage
if (
stage.state is CaptureStageState.AWAITING_CONFIRMATION
and stage.request_id is not None
and stage.request_id not in handled_request_ids
):
answer = (
input(f"{stage.prompt or 'Continue capture'} [Y/n]: ").strip().lower()
)
if answer in ("", "y", "yes"):
capture_control.confirm(stage.request_id)
else:
capture_control.cancel(stage.request_id)
raise RuntimeError("capture was cancelled")
handled_request_ids.add(stage.request_id)
calibration = calibration_progress.latest()
if (
calibration is not None
and calibration.value.job.state is CalibrationJobState.FAILED
):
failure = calibration.value.job.failure
message = failure.message if failure else "unknown error"
raise RuntimeError(f"calibration failed: {message}")
def main() -> None:
arguments = argparse.ArgumentParser(
description="Control one physical glove in a multi-glove domain"
)
arguments.add_argument("--left-port", required=True)
arguments.add_argument("--right-port", required=True)
arguments.add_argument("--updates", type=int, default=10)
arguments.add_argument("--target-name", default=DEFAULT_TARGET_NAME)
arguments.add_argument("--package-id", required=True)
options = arguments.parse_args()
if options.updates < 1:
arguments.error("--updates must be at least 1")
sdk = create_sdk(
options.left_port,
options.right_port,
options.target_name,
options.package_id,
)
glove = sdk.glove()
left = glove.device(name="left")
right = glove.device(side=Side.RIGHT)
left_output = left.output(name=options.target_name)
right_output = right.output(name=options.target_name)
left_capture_progress = left.capture_progress()
right_capture_progress = right.capture_progress()
left_calibration_progress = left.calibration_progress(name=options.target_name)
right_calibration_progress = right.calibration_progress(name=options.target_name)
left_capture_control = left.capture_control()
right_capture_control = right.capture_control()
left.start()
right.start()
def wait_for_output(
output: ReadChannel[HandJointCommand],
capture_progress: ReadChannel[CaptureProgress],
capture_control: CaptureControl,
calibration_progress: ReadChannel[CalibrationProgress],
) -> None:
handled_request_ids: set[str] = set()
deadline = time.monotonic() + 300.0
while output.latest() is None and time.monotonic() < deadline:
update = glove.update()
handle_capture_prompt(
capture_progress,
capture_control,
calibration_progress,
handled_request_ids,
)
update.sleep()
if output.latest() is None:
raise RuntimeError("retargeting output was not ready before the timeout")
wait_for_output(
left_output,
left_capture_progress,
left_capture_control,
left_calibration_progress,
)
wait_for_output(
right_output,
right_capture_progress,
right_capture_control,
right_calibration_progress,
)
for _ in range(options.updates):
update = glove.update()
update.sleep()
left_sample = left_output.latest()
if left_sample is not None:
print_output("left", left_sample)
right_sample = right_output.latest()
if right_sample is not None:
print_output("right", right_sample)
left_before_stop = left_output.latest()
if left_before_stop is None:
raise RuntimeError("both gloves must produce output before lifecycle control")
right.stop()
print("right device stopped; left remains active")
right_stopped_baseline = right_output.latest()
if right_stopped_baseline is None:
raise RuntimeError("right output was unavailable after stop")
left_sample = None
for _ in range(options.updates):
update = glove.update()
update.sleep()
right_candidate = right_output.latest()
if (
right_candidate is not None
and right_candidate.sequence > right_stopped_baseline.sequence
):
raise RuntimeError("right output advanced while the device was stopped")
candidate = left_output.latest()
if candidate is not None and candidate.sequence > left_before_stop.sequence:
left_sample = candidate
break
if left_sample is None:
raise RuntimeError("left output did not advance while right was stopped")
print("left output while right stopped")
print_output("left", left_sample)
right_before_restart = right_output.latest()
if right_before_restart is None:
raise RuntimeError("right output was unavailable before restart")
right.start()
print("right device restarted")
right_sample = None
for _ in range(options.updates):
update = glove.update()
update.sleep()
candidate = right_output.latest()
if candidate is not None and candidate.sequence > right_before_restart.sequence:
right_sample = candidate
break
if right_sample is None:
raise RuntimeError("right output did not advance after restart")
print("right output after restart")
print_output("right", right_sample)
health = glove.health()
print(
f"health ok={health.ok} warnings={health.warning_count} "
f"errors={health.error_count}"
)
glove.close()
if __name__ == "__main__":
main()Run (both a left and a right glove have to be connected):
python examples/glove/python/src/device_lifecycle.py \
--left-port <left-serial-port> --right-port <right-serial-port> --package-id <package-id>Expected output: the terminal prints the left and right hand samples and step messages such as
right device stopped; left remains active, left output while right stopped,
right device restarted and right output after restart; the script exits after the checks. For
the full mapping configuration, see Reading retargeting data.