SDKC++ SDK

快速开始

五个可直接运行的场景:原始姿态、Skeleton 与三种灵巧手映射。

当前版本 · 1.4.0

快速开始

这一章只放能直接跑的场景。每个场景对应一个独立示例文件,都在 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 计算,也不需要算法包:

文件:quick_start.cpp

零参数配置,用默认 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 是骨骼节点姿态。

读取灵巧手输出

三种映射的区别只在于手套与灵巧手的对应关系:

场景映射示例文件
一对一一只手套对应一个具名 targetforeground_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 数据。