Quick start
Five runnable scenarios: raw pose, Skeleton, and the three dexterous hand mappings.
Quick start
This chapter contains only runnable scenarios. Each scenario maps to a standalone example file in
examples/glove/cpp/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, set up the SDK package as described in Installation & requirements and connect a glove. The examples ship with their own CMake project, so build them once:
cmake -S examples/glove/cpp -B build/examples -DCMAKE_PREFIX_PATH=/absolute/path/to/qnbot-cpp-sdk
cmake --build build/examples --config ReleaseEach section below gives the executable to run. Every example stops with Ctrl+C.
Where to start: Run quick_start.cpp first to confirm that pose frames arrive continuously. For SDK-processed skeleton data, run skeleton.cpp. Once an algorithm package is installed, run foreground_runtime.cpp 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.cpp
Zero-argument configuration that auto-discovers the single supported glove with the default GloveConfig and receives pose frames through a callback. This is the shortest path:
#include <qnbot/glove.hpp>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <utility>
int main() {
try {
qnbot::SdkConfig config;
config.devices = {qnbot::GloveConfig{}};
qnbot::Sdk sdk(std::move(config));
auto glove = sdk.glove();
auto pose = glove.device().pose();
const auto subscription =
pose.subscribe([](const qnbot::Sample<qnbot::GlovePose>& sample) {
std::cout << "pose sequence=" << sample.sequence
<< " fingertips="
<< sample.value.payload.fingertip_local.size()
<< '\n';
});
glove.start();
std::cout << "running; press Ctrl+C to stop" << std::endl;
glove.run_forever();
static_cast<void>(subscription);
return EXIT_SUCCESS;
} catch (const std::exception& error) {
std::cerr << "qnbot quick start failed: " << error.what() << '\n';
return EXIT_FAILURE;
}
}Run:
./build/examples/quick_startExpected output (the SDK first prints two connection log lines, sdk_initialized and device_connected; this is the example's own output, the sequence number increases per frame and the fingertip count stays at 5):
running; press Ctrl+C to stop
pose sequence=1 fingertips=5
pose sequence=2 fingertips=5The process keeps running and prints continuously. If multiple gloves are found, use SerialConnection to select one, see Device discovery & connection.
Skeleton
File: skeleton.cpp
Adds SDK-processed data on top of pose reading: subscribes to the dynamic OpenXR hand 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.
#include <qnbot/glove.hpp>
#include "example_support.hpp"
#include <cstdlib>
#include <iostream>
#include <optional>
#include <string>
int main(int argc, char** argv) {
try {
std::string port;
std::optional<qnbot::Side> side;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--port") {
port = example::require_value(argc, argv, index, argument);
} else if (argument == "--side") {
side = example::parse_side(
example::require_value(argc, argv, index, argument));
} else {
throw std::invalid_argument("unknown argument: " + argument);
}
}
if (port.empty()) throw std::invalid_argument("--port is required");
if (!side) throw std::invalid_argument("--side is required");
qnbot::Sdk sdk(example::serial_config(port, *side));
auto glove = sdk.glove();
auto skeleton = glove.device().skeleton();
const auto joint_angles_subscription =
skeleton.joint_angles().subscribe(
[](const qnbot::Sample<qnbot::HandJointCommand>& sample) {
std::cout << "skeleton sequence=" << sample.sequence
<< " target=" << sample.value.target
<< " joints=" << sample.value.joints.size()
<< '\n';
});
const auto pose_subscription = skeleton.pose().subscribe(
[](const qnbot::Sample<qnbot::HandSkeletonPose>& sample) {
const auto& pose = sample.value;
std::cout << "pose sequence=" << sample.sequence
<< " frame=" << pose.coordinate_frame << " wrist=["
<< pose.positions_local[0][0] << ", "
<< pose.positions_local[0][1] << ", "
<< pose.positions_local[0][2] << "] index_tip=["
<< pose.positions_local[9][0] << ", "
<< pose.positions_local[9][1] << ", "
<< pose.positions_local[9][2] << "]\n";
});
glove.start();
std::cout << "running; press Ctrl+C to stop" << std::endl;
glove.run_forever();
static_cast<void>(joint_angles_subscription);
static_cast<void>(pose_subscription);
return EXIT_SUCCESS;
} catch (const std::exception& error) {
return example::report_error(error);
}
}Run:
./build/examples/skeleton --port <serial-port> --side rightExpected output (wrist stays at the wrist-local origin, the index_tip coordinates are elided):
running; press Ctrl+C to stop
skeleton sequence=1 target=hand.skeleton.right joints=25
pose sequence=1 frame=wrist_local wrist=[0, 0, 0] index_tip=[…, …, …]Both lines print once per frame: joints= in the skeleton line is the joint count (fixed at 25), while pose carries 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.cpp |
| One-to-many | One glove drives several dexterous hands | multiple_outputs.cpp |
| Many-to-many | Several gloves drive several dexterous hands | device_lifecycle.cpp |
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 subscribes to the target output.
#include "example_support.hpp"
#include <cstdlib>
#include <iostream>
int main(int argc, char** argv) {
try {
const auto options = example::parse_serial_options(
argc, argv, example::SerialExample::foreground);
qnbot::Sdk sdk(example::runtime_config(options));
auto glove = sdk.glove();
auto output = glove.device("primary").output(options.target_name);
const auto subscription = output.subscribe(example::print_output);
glove.start();
example::print_health("running", glove.health());
std::cout << "running; press Ctrl+C to stop" << std::endl;
glove.run_forever();
static_cast<void>(subscription);
return EXIT_SUCCESS;
} catch (const std::exception& error) {
return example::report_error(error);
}
}Run:
./build/examples/foreground_runtime --port <serial-port> --side right --package-id <package-id>Expected output (target defaults to hand.openxr_hand.<side>; <SN> is the glove serial number, the warnings/errors counters vary with link quality and joint values are elided):
running health ok=1 warnings=0 errors=0 devices=[glove.<SN>.right(connected=1, stale=0)]
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 named targets for the same source and reads each output by name; the glove connection and data stream stay single.
#include "example_support.hpp"
#include <cstdlib>
#include <iostream>
#include <optional>
#include <string>
namespace {
struct Options {
std::string port;
std::optional<qnbot::Side> side;
std::string package_id;
};
Options parse_options(int argc, char** argv) {
Options options;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--port") {
options.port = example::require_value(argc, argv, index, argument);
} else if (argument == "--side") {
options.side = example::parse_side(
example::require_value(argc, argv, index, argument));
} else if (argument == "--package-id") {
options.package_id =
example::require_value(argc, argv, index, argument);
} else {
throw std::invalid_argument("unknown argument: " + argument);
}
}
if (options.port.empty()) throw std::invalid_argument("--port is required");
if (!options.side) throw std::invalid_argument("--side is required");
if (options.package_id.empty()) {
throw std::invalid_argument("--package-id is required");
}
return options;
}
qnbot::SdkConfig make_config(const Options& options) {
auto config = example::serial_config(options.port, *options.side);
qnbot::TargetConfig target;
target.name = "primary";
target.side = options.side;
target.source =
qnbot::DeviceSelector{"glove", std::string("primary"), std::nullopt};
target.algorithms = {qnbot::TargetAlgorithm{options.package_id}};
config.targets.push_back(target);
target.name = "backup";
config.targets.push_back(std::move(target));
return config;
}
} // namespace
int main(int argc, char** argv) {
try {
const auto options = parse_options(argc, argv);
qnbot::Sdk sdk(make_config(options));
auto glove = sdk.glove();
auto device = glove.device("primary");
auto primary = device.output("primary");
auto backup = device.output("backup");
const auto primary_subscription = primary.subscribe(
[](const qnbot::Sample<qnbot::HandJointCommand>& sample) {
std::cout << "primary ";
example::print_output(sample);
});
const auto backup_subscription = backup.subscribe(
[](const qnbot::Sample<qnbot::HandJointCommand>& sample) {
std::cout << "backup ";
example::print_output(sample);
});
glove.start();
std::cout << "running; press Ctrl+C to stop" << std::endl;
glove.run_forever();
static_cast<void>(primary_subscription);
static_cast<void>(backup_subscription);
return EXIT_SUCCESS;
} catch (const std::exception& error) {
return example::report_error(error);
}
}Run:
./build/examples/multiple_outputs --port <serial-port> --side right --package-id <package-id>Expected output (both targets share one frame sequence and each target's output carries its own name; joint values elided):
running; press Ctrl+C to stop
primary retargeting sequence=1 target=hand.primary.right joints={…}
backup retargeting sequence=1 target=hand.backup.right joints={…}
primary retargeting sequence=2 target=hand.primary.right joints={…}The two callbacks can run on different threads, and one output line is assembled from several
std::cout writes, so real output can interleave inside a line (observed:
backup retargeting sequence=primary retargeting sequence=1 target=…); synchronise yourself when
you need whole lines. 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.
#include "example_support.hpp"
#include <cstdlib>
#include <chrono>
#include <iostream>
#include <optional>
#include <string>
#include <thread>
#include <unordered_set>
namespace {
struct Options {
std::string left_port;
std::string right_port;
std::uint64_t updates{10};
std::string target_name{example::default_target_name};
std::string package_id;
};
Options parse_options(int argc, char** argv) {
Options options;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--left-port") {
options.left_port =
example::require_value(argc, argv, index, argument);
} else if (argument == "--right-port") {
options.right_port =
example::require_value(argc, argv, index, argument);
} else if (argument == "--updates") {
options.updates = example::parse_positive_count(
example::require_value(argc, argv, index, argument), argument);
} else if (argument == "--target-name") {
options.target_name =
example::require_value(argc, argv, index, argument);
} else if (argument == "--package-id") {
options.package_id =
example::require_value(argc, argv, index, argument);
} else {
throw std::invalid_argument("unknown argument: " + argument);
}
}
if (options.left_port.empty()) {
throw std::invalid_argument("--left-port is required");
}
if (options.right_port.empty()) {
throw std::invalid_argument("--right-port is required");
}
if (options.left_port == options.right_port) {
throw std::invalid_argument(
"--left-port and --right-port must be different");
}
if (options.package_id.empty()) {
throw std::invalid_argument("--package-id is required");
}
return options;
}
qnbot::SdkConfig make_config(const Options& options) {
qnbot::SerialConnection left_connection;
left_connection.port = options.left_port;
qnbot::GloveConfig left{qnbot::Side::left, left_connection};
left.name = "left";
qnbot::SerialConnection right_connection;
right_connection.port = options.right_port;
qnbot::GloveConfig right{qnbot::Side::right, right_connection};
right.name = "right";
qnbot::TargetConfig left_target;
left_target.name = options.target_name;
left_target.side = qnbot::Side::left;
left_target.source =
qnbot::DeviceSelector{"glove", std::string("left"), std::nullopt};
left_target.algorithms = {qnbot::TargetAlgorithm{options.package_id}};
qnbot::TargetConfig right_target;
right_target.name = options.target_name;
right_target.side = qnbot::Side::right;
right_target.source =
qnbot::DeviceSelector{"glove", std::string("right"), std::nullopt};
right_target.algorithms = {qnbot::TargetAlgorithm{options.package_id}};
qnbot::SdkConfig config;
config.devices = {left, right};
config.targets = {left_target, right_target};
config.algorithms.capture.interaction =
qnbot::CaptureInteractionMode::external;
return config;
}
void print_output(const char* device_name,
const qnbot::Sample<qnbot::HandJointCommand>& sample) {
std::cout << device_name << ' ';
example::print_output(sample);
}
void handle_capture_prompt(
const qnbot::ReadChannel<qnbot::CaptureProgress>& capture_progress,
const qnbot::CaptureControl& capture_control,
const qnbot::ReadChannel<qnbot::CalibrationProgress>& calibration_progress,
std::unordered_set<std::string>& handled_request_ids) {
if (const auto capture = capture_progress.latest()) {
if (capture->value.session_state ==
qnbot::CaptureSessionState::failed) {
const auto message = capture->value.failure
? capture->value.failure->message
: "unknown error";
throw std::runtime_error("capture failed: " + message);
}
const auto& stage = capture->value.stage;
if (stage.state == qnbot::CaptureStageState::awaiting_confirmation &&
stage.request_id &&
handled_request_ids.count(*stage.request_id) == 0) {
std::cout << stage.prompt << " [Y/n]: ";
std::string answer;
std::getline(std::cin, answer);
if (answer.empty() || answer == "y" || answer == "Y" ||
answer == "yes" || answer == "YES") {
capture_control.confirm(*stage.request_id);
} else {
capture_control.cancel(*stage.request_id);
throw std::runtime_error("capture was cancelled");
}
handled_request_ids.insert(*stage.request_id);
}
}
if (const auto calibration = calibration_progress.latest();
calibration &&
calibration->value.job.state == qnbot::CalibrationJobState::failed) {
const auto message = calibration->value.job.failure
? calibration->value.job.failure->message
: "unknown error";
throw std::runtime_error("calibration failed: " + message);
}
}
} // namespace
int main(int argc, char** argv) {
try {
const auto options = parse_options(argc, argv);
qnbot::Sdk sdk(make_config(options));
auto glove = sdk.glove();
auto left = glove.device("left");
auto right = glove.device(qnbot::Side::right);
auto left_output = left.output(options.target_name);
auto right_output = right.output(options.target_name);
auto left_capture_progress = left.capture_progress();
auto right_capture_progress = right.capture_progress();
auto left_calibration_progress =
left.calibration_progress(options.target_name);
auto right_calibration_progress =
right.calibration_progress(options.target_name);
auto left_capture_control = left.capture_control();
auto right_capture_control = right.capture_control();
left.start();
right.start();
const auto wait_for_output =
[&](const auto& output, const auto& capture_progress,
const auto& capture_control, const auto& calibration_progress) {
std::unordered_set<std::string> handled_request_ids;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(300);
while (!output.latest() &&
std::chrono::steady_clock::now() < deadline) {
const auto update = glove.update();
handle_capture_prompt(capture_progress, capture_control,
calibration_progress,
handled_request_ids);
static_cast<void>(update.sleep());
}
if (!output.latest()) {
throw std::runtime_error(
"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 (std::uint64_t index = 0; index < options.updates; ++index) {
const auto update = glove.update();
static_cast<void>(update.sleep());
if (const auto sample = left_output.latest()) {
print_output("left", *sample);
}
if (const auto sample = right_output.latest()) {
print_output("right", *sample);
}
}
const auto left_before_stop = left_output.latest();
if (!left_before_stop) {
throw std::runtime_error(
"both gloves must produce output before lifecycle control");
}
right.stop();
std::cout << "right device stopped; left remains active\n";
const auto right_stopped_baseline = right_output.latest();
if (!right_stopped_baseline) {
throw std::runtime_error("right output was unavailable after stop");
}
std::optional<qnbot::Sample<qnbot::HandJointCommand>> left_sample;
for (std::uint64_t index = 0; index < options.updates; ++index) {
const auto update = glove.update();
static_cast<void>(update.sleep());
const auto right_candidate = right_output.latest();
if (right_candidate &&
right_candidate->sequence > right_stopped_baseline->sequence) {
throw std::runtime_error(
"right output advanced while the device was stopped");
}
const auto candidate = left_output.latest();
if (candidate && candidate->sequence > left_before_stop->sequence) {
left_sample = candidate;
break;
}
}
if (!left_sample) {
throw std::runtime_error(
"left output did not advance while right was stopped");
}
std::cout << "left output while right stopped\n";
print_output("left", *left_sample);
const auto right_before_restart = right_output.latest();
if (!right_before_restart) {
throw std::runtime_error(
"right output was unavailable before restart");
}
right.start();
std::cout << "right device restarted\n";
std::optional<qnbot::Sample<qnbot::HandJointCommand>> right_sample;
for (std::uint64_t index = 0; index < options.updates; ++index) {
const auto update = glove.update();
static_cast<void>(update.sleep());
const auto candidate = right_output.latest();
if (candidate &&
candidate->sequence > right_before_restart->sequence) {
right_sample = candidate;
break;
}
}
if (!right_sample) {
throw std::runtime_error(
"right output did not advance after restart");
}
std::cout << "right output after restart\n";
print_output("right", *right_sample);
example::print_health(glove.health());
glove.close();
return EXIT_SUCCESS;
} catch (const std::exception& error) {
return example::report_error(error);
}
}Run (both a left and a right glove have to be connected):
./build/examples/device_lifecycle \
--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, then one final health line before it
exits (joint values elided):
left retargeting sequence=… target=hand.openxr_hand.left joints={…}
right retargeting sequence=… target=hand.openxr_hand.right joints={…}
right device stopped; left remains active
left output while right stopped
left retargeting sequence=… target=hand.openxr_hand.left joints={…}
right device restarted
right output after restart
right retargeting sequence=… target=hand.openxr_hand.right joints={…}
health ok=1 warnings=0 errors=0With only one glove attached, or with the same glove port passed for both roles, startup fails outright:
qnbot example failed: configured left-hand glove does not match reported right-hand DeviceInfoFor the full mapping configuration, see Reading retargeting data.