快速开始
五个可直接运行的场景:原始姿态、Skeleton 与三种灵巧手映射。
快速开始
这一章只放能直接跑的场景。每个场景对应一个独立示例文件,都在
examples/glove/cpp/src/,互不依赖,可以只跑你需要的那个。全部示例(含每个文件对应的主要 API)见 示例索引。
开始前先按 安装与环境要求 配好 SDK 包并连好手套。示例工程自带 CMake 工程,构建一次即可:
cmake -S examples/glove/cpp -B build/examples -DCMAKE_PREFIX_PATH=/absolute/path/to/qnbot-cpp-sdk
cmake --build build/examples --config Release下面每节直接给出要执行的可执行文件。所有示例都用 Ctrl+C 结束。
建议从这里开始:首次接入先运行 quick_start.cpp,确认能持续收到姿态;需要 SDK 处理后的骨骼数据时运行 skeleton.cpp;已安装算法包并需要灵巧手输出时运行 foreground_runtime.cpp。每个示例的命令和预期输出都在下文。
读取原始姿态数据
姿态由设备直接产出,不经 SDK 计算,也不需要算法包:
零参数配置,用默认 GloveConfig 自动发现唯一连接的受支持手套,用 callback 逐帧接收姿态。这是最短的一条路径:
#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;
}
}运行:
./build/examples/quick_start预期输出(SDK 启动时会先打印 sdk_initialized 与 device_connected 两条连接日志,这里是示例自己的输出;序号逐帧递增,指尖数量固定为 5):
running; press Ctrl+C to stop
pose sequence=1 fingertips=5
pose sequence=2 fingertips=5进程不退出,持续打印。如果发现多只手套,改用 SerialConnection 固定选择,见
设备发现与连接。
读取 Skeleton
文件:skeleton.cpp
在读取姿态的基础上增加 SDK 处理后的数据:读取动态 OpenXR 手部骨骼的关节角和节点姿态。 Skeleton 由 SDK 提供,不需要算法包;首次运行需要完成一次采集标定,之后直接复用。定义见 读取 Retargeting 数据。
持续读取时按 运行方式 推进 SDK 调度。
#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);
}
}运行:
./build/examples/skeleton --port <串口> --side right预期输出(wrist 固定在 wrist-local 原点,index_tip 坐标已省略):
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=[…, …, …]两条线各印一行:skeleton 的 joints= 是关节数量(固定 25),pose 是骨骼节点姿态。
读取灵巧手输出
三种映射的区别只在于手套与灵巧手的对应关系:
| 场景 | 映射 | 示例文件 |
|---|---|---|
| 一对一 | 一只手套对应一个具名 target | foreground_runtime.cpp |
| 一对多 | 一只手套驱动多个灵巧手 | multiple_outputs.cpp |
| 多对多 | 多只手套驱动多个灵巧手 | device_lifecycle.cpp |
三种映射都需要先安装算法包,见 读取 Retargeting 数据——这些值由算法包按 target 做 retargeting 计算得出。首次运行还需要完成一次采集标定,SDK 会按算法包的提示引导,完成后会被后续运行复用。
一对一:一只手套对应一个具名 target
在当前线程持续运行,并订阅目标输出。
#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);
}
}运行:
./build/examples/foreground_runtime --port <串口> --side right --package-id <package-id>预期输出(target 默认是 hand.openxr_hand.<side>;<SN> 是手套序列号,warnings/errors 计数随连接质量变化,关节值已省略):
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={…}一直收不到输出时,确认算法包 ID、target 配置和标定状态,见 连接成功但收不到数据。
一对多:一只手套驱动多个灵巧手
为同一个 source 配置多个具名 target,再按名称分别读取输出;手套连接和数据流仍然只有一份。
#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);
}
}运行:
./build/examples/multiple_outputs --port <串口> --side right --package-id <package-id>预期输出(两个 target 共用同一帧序号,每个 target 的输出带自己的名称,关节值已省略):
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={…}两个 callback 可能由不同线程并发调用,而一个输出行由多次 std::cout 写入拼成,因此实际输出可能在同一行内交错(实测出现过 backup retargeting sequence=primary retargeting sequence=1 target=…
这样的行);需要成行输出时自己加锁。完整的 target 配置见 读取 Retargeting 数据。
多对多:多只手套驱动多个灵巧手
两只手套各自配置 target,选择各自的设备并读取自己的输出。脚本会先等待左右手输出,然后停止右手设备确认左手继续推进,再重启右手。
#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);
}
}运行(需要同时接上左右手两只手套):
./build/examples/device_lifecycle \
--left-port <左手串口> --right-port <右手串口> --package-id <package-id>预期输出:终端分别打印左右手输出,并打印 right device stopped; left remains active、
left output while right stopped、right device restarted、right output after restart
等步骤提示,最后打印一次 health 后退出(关节值已省略):
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=0只接一只手、或把同一只手的串口填给两个 --*-port 时,启动阶段就会失败:
qnbot example failed: configured left-hand glove does not match reported right-hand DeviceInfo完整映射配置见 读取 Retargeting 数据。