Vyntek

端到端最小闭环:headless_run

标准示例文档:源码清单、学习路线、逐段讲解、常见误区、双平台构建、真实输出与退出码。

用途

用公开头跑通最短路径:RegisterBuiltinTools() → ToolGroup::Create → AddTool → RunSession::Run(),再把「工具是否在途」接到采集端联合停流门禁,最后由宿主显式停两端。

读完应能回答三个问题:谁拥有流与工具?Run 的返回值怎么读?为什么停相机必须由宿主显式做?

源码

  • examples/headless_run.cpp —— 入口 main():建组、装载工具、跑一次、取消、停两端(片段 1–5) · 查看完整源文件
  • examples/headless_camera.cpp —— 宿主侧相机接线实现:ArmCamera / WireToolWorkHook / StopCameraExplicitly(片段 6–8) · 查看完整源文件
  • examples/headless_camera.hpp —— 上述三函数的声明(宿主集成示范的公开外形) · 查看完整源文件
  • examples/CMakeLists.txt —— headless_run 目标:链接 Vyntek::toolgroup,产物进 build/<preset>/bin/ · 查看完整源文件

下文片段均为源码节选,标注行号,与仓库当前源码逐字一致。

学习路线

按四步读,每步对应下文片段:

  1. 对象从哪来(片段 1):ToolGroup 是 shared_ptr,工具是 unique_ptr 且被流接管,RunSession 只观察。
  2. 两个 session 如何分工(片段 2、6、7):编排侧与采集侧各自独立;钩子只上报「工具在途」,不碰采集状态。
  3. 结果怎么读(片段 3、4):先看 RunStatus,再看 diagnostics;GetOutput 是标量投影,不是保留集。
  4. 怎么退出(片段 5、8):取消是协作请求;停流是宿主的显式动作,且必须等到 Stopped。

片段 1 · 建组、注册、装载(headless_run.cpp:16-32)

查看完整源文件
    using namespace vyntek::toolgroup;
    using vyntek::imaging::AcquisitionSession;
    auto group = ToolGroup::Create("headless");
    if (!group) {
        std::fprintf(stderr, "ToolGroup::Create failed\n");
        return 1;
    }

    ToolRegistry registry;
    registry.RegisterBuiltinTools();
    auto tool = registry.Create("passthrough", "t1");
    if (!tool || !group->AddTool(std::move(tool))) {
        std::fprintf(stderr, "passthrough AddTool failed\n");
        return 2;
    }

    RunSession session(group);

讲解:

  • ToolGroup::Create 返回 std::shared_ptr<ToolGroup>,空即失败——先判空再解引用。
  • RegisterBuiltinTools() 注册内建工具;当前只有 passthrough 这个契约夹具,它的 Run 是基类桩(只报 NotImplemented)。
  • registry.Create(...) 返回 std::unique_ptr<Tool>;group->AddTool(std::move(tool)) 接管所有权并返回非拥有的观察指针。判空用返回值,不要再用已 move 的 tool。
  • RunSession session(group) 只观察流,不取得流的所有权。

片段 2 · 把工具执行接到采集端(headless_run.cpp:34-40)

查看完整源文件
    // DEBT-IMG-004 host wiring sketch: drive the acquisition joint stop gate from real tool
    // execution instead of the skeleton's default-true flag.
    AcquisitionSession camera;
    if (const int rc = headless::ArmCamera(camera, "headless-cam"); rc != 0) {
        return rc;
    }
    headless::WireToolWorkHook(session, camera);

讲解:

  • AcquisitionSession 是采集端对象;RunSession 是编排端对象。两者生命周期独立,示例里并列构造。
  • ArmCamera 内部先 Arm 再 Start(片段 6),失败直接以 6 退出。
  • WireToolWorkHook 只做一件事:把「工具在途」回调转成 camera.SetToolWorkDone(bool)(片段 7)。它不停止相机。

片段 3 · 读取 Run 结果(headless_run.cpp:42-46)

查看完整源文件
    const RunResult result = session.Run();
    if (result.status != RunStatus::Succeeded) {
        std::fprintf(stderr, "Run status %d\n", static_cast<int>(result.status));
        return 3;
    }

讲解:

  • Run() 同步执行,返回 RunResult。判成功用 status == RunStatus::Succeeded(或 result.ok())。
  • 失败细节看 result.diagnostics,message 只是摘要;failed_tool_id 指出是哪个工具触发的 abort。
  • Busy 表示「另一个 Run 仍在途」,不是失败:先结束在途那次,不要忙等重试。

片段 4 · GetOutput 的误区(headless_run.cpp:48-52)

查看完整源文件
    // GetOutput is not KeepLatest retention (output-inspection).
    if (!session.GetOutput("t1.out").empty()) {
        std::fprintf(stderr, "GetOutput stub must stay empty\n");
        return 4;
    }

讲解:

  • GetOutput 只做标量投影;图像类输出没有字符串形式,因此这里必须是空串。空串不代表 Run 失败。
  • GetOutput 不是 KeepLatest 保留语义;保留集/观察入口在 toolgroup/output_inspector.hpp(见 output-inspection)。
  • 这条断言防止有人把「观察」误接到「执行结果」上。

片段 5 · 取消是请求,不是停流(headless_run.cpp:54-58)

查看完整源文件
    session.RequestCancel();
    if (!session.cancel_requested()) {
        std::fprintf(stderr, "RequestCancel did not latch\n");
        return 5;
    }

讲解:

  • RequestCancel() 只置位协作取消;cancel_requested() 反映「已请求」,不保证工具已退出。
  • 取消对采集端没有任何作用:它不会替你 RequestStop。

片段 6 · 采集端启动(headless_camera.cpp:9-15)

查看完整源文件
int ArmCamera(vyntek::imaging::AcquisitionSession & camera, const char * name) {
    if (camera.Arm(name) != vyntek::imaging::Status::Ok || camera.Start() != vyntek::imaging::Status::Ok) {
        std::fprintf(stderr, "AcquisitionSession Arm/Start failed\n");
        return 6;
    }
    return 0;
}

讲解:Arm 建立命名会话,Start 进入 Running。两步都用 Status 返回,示例用 OR 短路——任一失败都不继续。

片段 7 · 钩子只上报(headless_camera.cpp:17-19)

查看完整源文件
void WireToolWorkHook(vyntek::toolgroup::RunSession & session, vyntek::imaging::AcquisitionSession & camera) {
    session.SetToolWorkDoneHook([&camera](bool tool_work_done) { camera.SetToolWorkDone(tool_work_done); });
}

讲解:

  • 回调参数 tool_work_done:进入工具执行发 false,退出执行发 true,覆盖正常结束、工具失败、协作取消、缺流、Validate 失败全部路径。
  • lambda 捕获 camera 的引用:相机必须活得比 hook 久。示例里 camera 在 main 栈上,顺序安全。
  • Busy 早返回时钩子不武装:否则会误报「工具已结束」而错误解锁停流门禁。

片段 8 · 宿主显式停流(headless_camera.cpp:21-38)

查看完整源文件
int StopCameraExplicitly(vyntek::imaging::AcquisitionSession & camera) {
    // The hook did NOT stop the camera — that is the host's job, done explicitly.
    if (camera.state() != vyntek::imaging::AcquisitionState::Running) {
        std::fprintf(stderr, "tool-work hook must not stop the acquisition end\n");
        return 7;
    }
    if (!camera.stop_progress().tool_work_done) {
        std::fprintf(stderr, "tool work must be reported done after Run returns\n");
        return 8;
    }
    if (camera.RequestStop() != vyntek::imaging::Status::Ok ||
        camera.WaitStop(vyntek::imaging::StopTimeout{ 1000 }) != vyntek::imaging::Status::Ok ||
        camera.state() != vyntek::imaging::AcquisitionState::Stopped) {
        std::fprintf(stderr, "explicit host stop must reach Stopped\n");
        return 9;
    }
    return 0;
}

讲解(退出码 7/8/9 就是这三条断言的编号):

  • state() == Running:证明 Run 返回后钩子没有把相机停掉(M3-07 / TC-SESS-05:两端不自动联动)。
  • stop_progress().tool_work_done:证明工具工作已上报完成,联合门禁可以解锁。
  • RequestStop() + WaitStop(StopTimeout{1000}) 到 Stopped:停流是宿主显式动作;超时不会伪造 Stopped。

常见误区

  • 「Run 成功就说明算法算完了」 —— passthrough 是夹具,基类 Run 只报 NotImplemented;空组 Run 仍可 Succeeded
  • 「RequestCancel 会顺便停相机」 —— 不会。两端各停各的,宿主必须显式调两次
  • 「Latest() 是队列头」 —— Latest() 是所有权预览槽(最近一次 TryPostOwned 的 Share);FIFO 头是 PeekIngressFront()
  • 「GetOutput 空串 = 没跑成功」 —— 空串是投影限制,不是状态

构建与运行

VYNTEK_BUILD_EXAMPLES=ON 才会构建示例;ninja-debug-all 预设已同时打开 tests 与 examples。二进制统一落在 build/ninja-debug-all/bin/。

Linux(本机 VPS,AGENTS.md「本机 VPS」小节)

cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target headless_run
./build/ninja-debug-all/bin/headless_run

Windows(PowerShell,AGENTS.md「构建」小节,仓库在 D:\Works\Vyntek)

cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target headless_run
.\build\ninja-debug-all\bin\headless_run.exe

若坚持用 AGENTS.md 的单预设写法,需显式加开关: cmake --preset ninja-debug -DVYNTEK_BUILD_TESTS=ON -DVYNTEK_BUILD_EXAMPLES=ON。

输出

本机实跑(Linux,2026-10-22):

$ ./build/ninja-debug-all/bin/headless_run
vyntek 0.1.0 (commit <build-sha>)
headless_run ok
$ echo $?
0

<build-sha> 是构建时的 HEAD(示例编译进二进制),因此与阅读时的仓库提交不一定相同。

退出码

  • 0 —— 全部步骤通过,打印 headless_run ok
  • 1 —— ToolGroup::Create("headless") 返回空
  • 2 —— registry.Create("passthrough", "t1") 或 AddTool 失败
  • 3 —— Run() 结果不是 Succeeded(细节看 RunResult.diagnostics)
  • 4 —— GetOutput("t1.out") 非空——标量投影不应有值
  • 5 —— RequestCancel() 后 cancel_requested() 未置位
  • 6 —— AcquisitionSession::Arm / Start 失败
  • 7 —— Run 结束后采集端已不是 Running——钩子不得自动停相机(M3-07)
  • 8 —— stop_progress().tool_work_done 为 false——工具工作未上报完成
  • 9 —— 显式 RequestStop + WaitStop(1000) 未到 Stopped

7/8/9 不是普通错误码,而是契约断言:任何一条触发,说明「两端停止互不联动」这条边界被改坏了。

On this page